├── .browserlistrc ├── .editorconfig ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── Tests ├── .gitkeep ├── Demo │ └── DemoTest.php └── PHPUnit.php ├── aliases.sh ├── babel.config.js ├── bin └── selenium-server-standalone-3.9.1.jar ├── composer.json ├── cypress.json ├── db └── migrations │ ├── 20190119184642_create_users.sql │ ├── 20190119184740_create_user_remember.sql │ ├── 20190119184757_create_user_permissions.sql │ └── 20201027162730_2fa.sql ├── dev ├── .htaccess ├── .routeLoaderGenerator.php ├── Actions │ ├── Action.php │ ├── Auth.php │ ├── Cookies.php │ ├── Csrf.php │ ├── FileSystem.php │ ├── Flash.php │ ├── Hash.php │ ├── PostValidator.php │ ├── Random.php │ ├── Response.php │ └── TwoFactor.php ├── Controllers │ ├── AuthController.php │ ├── Controller.php │ ├── DemoController.php │ └── TwoFactorController.php ├── Exceptions │ ├── CsrfTokenMismatch.php │ ├── Invalid2FA.php │ └── LegacyPhpError.php ├── Filters │ ├── AdminFilter.php │ ├── ComposedFilter.php │ ├── Filter.php │ ├── LogoutFilter.php │ ├── UserFilter.php │ └── VisitorFilter.php ├── Handlers │ ├── ExceptionHandler.php │ └── LegacyPhpErrorHandler.php ├── Helpers │ ├── AppEnv.php │ ├── Path.php │ ├── Routing.php │ ├── TwigExtensions │ │ ├── AuthExtension.php │ │ ├── CsrfExtension.php │ │ ├── FlashExtension.php │ │ └── PathExtension.php │ ├── UserResponsePair.php │ └── WhiteboxFeatures │ │ ├── AbstractDirectoryBrowser.php │ │ ├── ArrayHelper.php │ │ ├── I_FSBrowser.php │ │ ├── I_JsonSerializable.php │ │ ├── I_MagicalArrayable.php │ │ ├── MagicalArray.php │ │ ├── RecursiveDirectoryBrowser.php │ │ ├── RouteLoader.php │ │ └── T_RouteLoader.php ├── Middlewares │ ├── AcceptsRedirect.php │ ├── Auth.php │ ├── Csrf.php │ ├── Middleware.php │ ├── RedirectAfterRequest.php │ ├── RequestBinding.php │ ├── Requires2FA.php │ └── RouteModelBinding.php ├── Models │ ├── Admin.php │ ├── Permission.php │ ├── Role.php │ ├── RolePermission.php │ ├── TwoFactor.php │ ├── User.php │ ├── UserRemember.php │ └── UserRole.php ├── bootstrap.php ├── config.php ├── config │ ├── Config.php │ ├── development.php │ └── production.php ├── container.php ├── container │ └── definitions.php ├── db.php ├── dumpRouteLoader.sh ├── env.php ├── js │ ├── 2fa.js │ ├── demo.js │ ├── e2e │ │ ├── .eslintrc.js │ │ ├── fixtures │ │ │ ├── .gitkeep │ │ │ └── example.json │ │ ├── plugins │ │ │ └── index.js │ │ ├── support │ │ │ ├── commands.js │ │ │ └── index.js │ │ └── tests │ │ │ ├── demo.e2e.js │ │ │ └── examples │ │ │ ├── actions.e2e.js │ │ │ ├── aliasing.e2e.js │ │ │ ├── assertions.e2e.js │ │ │ ├── connectors.e2e.js │ │ │ ├── cookies.e2e.js │ │ │ ├── cypress_api.e2e.js │ │ │ ├── files.e2e.js │ │ │ ├── local_storage.e2e.js │ │ │ ├── location.e2e.js │ │ │ ├── misc.e2e.js │ │ │ ├── navigation.e2e.js │ │ │ ├── network_requests.e2e.js │ │ │ ├── querying.e2e.js │ │ │ ├── spies_stubs_clocks.e2e.js │ │ │ ├── traversal.e2e.js │ │ │ ├── utilities.e2e.js │ │ │ ├── viewport.e2e.js │ │ │ ├── waiting.e2e.js │ │ │ └── window.e2e.js │ ├── mainDemo.js │ ├── tests │ │ ├── .eslintrc.js │ │ ├── .gitkeep │ │ └── demo.test.js │ ├── utils.js │ └── utils │ │ └── BrowserDetector.js ├── middlewares.php ├── resources │ ├── favicon.png │ └── img │ │ ├── jpg │ │ └── .gitkeep │ │ ├── png │ │ └── .gitkeep │ │ └── svg │ │ └── .gitkeep ├── routes │ ├── 2fa.php │ ├── auth.php │ ├── demo.php │ ├── demo2.php │ ├── demo3.php │ └── redir.php ├── scss │ ├── .gitkeep │ ├── components │ │ ├── .gitkeep │ │ └── Hamburger.scss │ ├── libraries │ │ ├── _hamburger.scss │ │ ├── _mq.scss │ │ └── index.scss │ └── mixins │ │ ├── _browser.scss │ │ ├── _margin.scss │ │ ├── _padding.scss │ │ ├── _radius.scss │ │ └── index.scss ├── views │ ├── .partials │ │ ├── layout.twig │ │ └── modules │ │ │ ├── demo.twig │ │ │ ├── favicons.twig │ │ │ ├── flash.twig │ │ │ ├── globalCSS.twig │ │ │ └── globalJS.twig │ ├── 2fa.twig │ ├── auth │ │ ├── login.twig │ │ └── register.twig │ ├── demo.twig │ ├── demo2.twig │ ├── demo3.twig │ └── errors │ │ ├── 400.twig │ │ ├── 401.twig │ │ ├── 403.twig │ │ ├── 404.twig │ │ └── 500.twig └── vue │ ├── components │ ├── Demo.vue │ ├── Hamburger.vue │ └── QrCode.vue │ ├── index.js │ ├── plugins │ ├── flash.js │ ├── index.js │ ├── indexedDB.js │ ├── json.js │ ├── localStorage.js │ ├── mediaQueries.js │ ├── vue-router.js │ └── vuex.js │ ├── router │ └── index.js │ ├── store │ ├── index.js │ ├── modules │ │ ├── .gitkeep │ │ └── counter │ │ │ ├── actions.js │ │ │ ├── getters.js │ │ │ ├── index.js │ │ │ ├── mutations.js │ │ │ └── state.js │ └── utils.js │ └── utils.js ├── eslint-rules └── .gitkeep ├── jest.config.js ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public_html ├── .htaccess ├── assets │ ├── css │ │ ├── .gitkeep │ │ ├── clear_float.css │ │ ├── demo.css │ │ ├── demo.css.gz │ │ ├── font_loader.css │ │ └── reset.css │ ├── fonts │ │ ├── raleway.ttf │ │ └── roboto.ttf │ ├── img │ │ ├── .gitkeep │ │ └── favicons │ │ │ ├── .gitkeep │ │ │ ├── apple-touch-icon.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon.ico │ │ │ └── favicon.ico.gz │ ├── js │ │ ├── 2fa.bundle.js │ │ ├── 2fa.bundle.js.gz │ │ ├── demo.bundle.js │ │ └── demo.bundle.js.gz │ └── manifest.json ├── index.php └── uploads │ ├── .gitkeep │ └── demo.json └── webpack.config.js /.browserlistrc: -------------------------------------------------------------------------------- 1 | defaults 2 | not IE_Mob 11 3 | not dead 4 | last 4 versions -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | 9 | [*.md] 10 | trim_trailing_whitespace = false 11 | 12 | [*.{yml,yaml}] 13 | indent_size = 2 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | #cf. https://github.com/turnitin/dbmate#usage 2 | DATABASE_URL = "://:@
:/" 3 | 4 | # "production" or "development" or "test" 5 | DEV_ENV = "production" 6 | 7 | NODE_ENV = "${DEV_ENV}" 8 | PHP_ENV = "${DEV_ENV}" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | bkp/ 2 | public_html/ 3 | vendor/ 4 | dev/js/tests/ 5 | dev/js/e2e/ 6 | 7 | node_modules/ 8 | eslint-rules/ 9 | templates/ 10 | dist/ 11 | ./* 12 | !./src/* 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dev/js/e2e/screenshots/ 2 | dev/js/e2e/videos/ 3 | 4 | logs/ 5 | bkp/ 6 | tmp/ 7 | .idea/ 8 | .vscode/ 9 | vendor/ 10 | coverage/ 11 | reports/ 12 | tests_output/ 13 | package-lock.json 14 | composer.lock 15 | route_autoload.php 16 | .brackets.json 17 | schema.sql 18 | yarn.lock 19 | .phpunit* 20 | 21 | 22 | 23 | 24 | # Logs 25 | logs 26 | *.log 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | 31 | # Runtime data 32 | pids 33 | *.pid 34 | *.seed 35 | *.pid.lock 36 | 37 | # Directory for instrumented libs generated by jscoverage/JSCover 38 | lib-cov 39 | 40 | # Coverage directory used by tools like istanbul 41 | coverage 42 | 43 | # nyc test coverage 44 | .nyc_output 45 | 46 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 47 | .grunt 48 | 49 | # Bower dependency directory (https://bower.io/) 50 | bower_components 51 | 52 | # node-waf configuration 53 | .lock-wscript 54 | 55 | # Compiled binary addons (https://nodejs.org/api/addons.html) 56 | build/Release 57 | 58 | # Dependency directories 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Typescript v1 declaration files 63 | typings/ 64 | 65 | # Optional npm cache directory 66 | .npm 67 | 68 | # Optional eslint cache 69 | .eslintcache 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variables file 81 | .env 82 | 83 | composer.phar 84 | /vendor/ 85 | 86 | # Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control 87 | # You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file 88 | # composer.lock 89 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.13.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Voltra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/Tests/.gitkeep -------------------------------------------------------------------------------- /Tests/Demo/DemoTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/PHPUnit.php: -------------------------------------------------------------------------------- 1 | /dev/null 4 | INIT_SOURCE=${BASH_SOURCE[0]} 5 | if [ -z $INIT_SOURCE ];then 6 | INIT_SOURCE=$(echo .) 7 | fi 8 | 9 | BASEDIR="${INIT_SOURCE}" 10 | if ([ -h "${BASEDIR}" ]); then 11 | while ([ -h "${BASEDIR}" ]);do 12 | cd $(dirname "$BASEDIR"); 13 | BASEDIR=$(readlink "${BASEDIR}"); 14 | done 15 | fi 16 | cd $(dirname ${BASEDIR}) > /dev/null 17 | BASEDIR=$(pwd) 18 | \popd > /dev/null 19 | 20 | 21 | ##Aliases definition 22 | alias phpunit='${BASEDIR}/vendor/bin/phpunit' 23 | alias punit='phpunit --testdox' 24 | alias dumpRouteLoader='\pushd ${BASEDIR}/dev > /dev/null && ./dumpRouteLoader.sh && \popd > /dev/null' 25 | alias npmr='npm run' 26 | 27 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | 3 | /** 4 | * @type {import("@babel/core").TransformOptions} 5 | */ 6 | module.exports = { 7 | exclude: [ 8 | /\bcore-js\b/i, 9 | /\bwebpack\b/, 10 | /\bregenerator-runtime\b/, 11 | ], 12 | presets: [ 13 | [ 14 | "@babel/preset-env", 15 | /** @type {typeof import("@babel/preset-env")._default} */ 16 | { 17 | useBuiltIns: "usage", // polyfills on use 18 | corejs: 3, // use core-js@3 19 | modules: "umd", 20 | }, 21 | ], 22 | [ 23 | "@vue/babel-preset-jsx", 24 | { 25 | functional: true, 26 | injectH: true, 27 | vModel: true, 28 | vOn: true, 29 | }, 30 | ], 31 | ], 32 | plugins: [ 33 | "@babel/plugin-proposal-function-bind", // array.map(::this.myMethod) 34 | "@babel/plugin-proposal-class-properties", // class { myMember = 10; } 35 | "@babel/plugin-proposal-object-rest-spread", // {...myObj} 36 | "@babel/plugin-proposal-do-expressions", // const two = do{ if(false) 0; else 2;s } 37 | "@babel/plugin-proposal-numeric-separator", // const twoK = 2_000; 38 | "@babel/plugin-proposal-nullish-coalescing-operator", // const stuff = null ?? "stuff"; 39 | "@babel/plugin-proposal-optional-chaining", // window?.might?.have?.this?.property?.doStruff(); 40 | "@babel/plugin-proposal-throw-expressions", // onError(() => throw new TypeError("OnO")); 41 | [ 42 | "@babel/plugin-proposal-pipeline-operator", { // '1' |> parseFloat |> timesTwo |> plusFourty 43 | proposal: "fsharp", 44 | }, 45 | ], 46 | "@babel/plugin-proposal-partial-application", // doStuff(?, 42) is the new (x => doStuff(x, 42)) 47 | "@babel/plugin-syntax-dynamic-import", // import("myComponent.vue").then(...) 48 | // 49 | "@babel/plugin-syntax-jsx", 50 | "babel-plugin-transform-jsx", 51 | "babel-plugin-jsx-v-model", 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /bin/selenium-server-standalone-3.9.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/bin/selenium-server-standalone-3.9.1.jar -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "voltra/slim-vue-app", 3 | "version": "2.1.0", 4 | "description": "A good to go project setup for Slim VueJS Twig applications", 5 | "type": "project", 6 | "authors": [ 7 | { 8 | "name": "Voltra", 9 | "email": "ludwig.guerin.98@gmx.fr" 10 | } 11 | ], 12 | "minimum-stability": "stable", 13 | "autoload": { 14 | "files": [ 15 | "dev/Filters/ComposedFilter.php", 16 | "dev/Middlewares/Requires2FA.php" 17 | ], 18 | "psr-4": { 19 | "App\\": "dev", 20 | "App\\Tests\\": "tests" 21 | } 22 | }, 23 | "require": { 24 | "php": ">=7.2.0", 25 | "ext-json": "*", 26 | "slim/slim": "^4.5", 27 | "vlucas/phpdotenv": "^5.1", 28 | "slim/psr7": "^1.2", 29 | "illuminate/database": "^8.1", 30 | "nesbot/carbon": "^2.39", 31 | "illuminate/filesystem": "^8.1", 32 | "illuminate/pagination": "^8.1", 33 | "kitetail/zttp": "^0.6.0", 34 | "hassankhan/config": "^2.1", 35 | "dflydev/fig-cookies": "^2.0", 36 | "slim/flash": "^0.4.0", 37 | "slim/twig-view": "^3.1", 38 | "kanellov/slim-twig-flash": "^0.2.0", 39 | "php-di/php-di": "^6.2", 40 | "php-di/slim-bridge": "^3.0", 41 | "middlewares/trailing-slash": "^2.0", 42 | "monolog/monolog": "^2.1", 43 | "oscarotero/env": "^2.1", 44 | "doctrine/annotations": "^1.10", 45 | "zeuxisoo/slim-whoops": "^0.7.2", 46 | "twig/twig": "^v3.0", 47 | "paragonie/random_compat": "^2.0", 48 | "paragonie/random-lib": "^2.0", 49 | "bryanjhv/slim-session": "^4.1", 50 | "lukasoppermann/http-status": "^2.0", 51 | "kelunik/two-factor": "^1.1", 52 | "robthree/twofactorauth": "^1.7", 53 | "league/flysystem": "^1.1", 54 | "voltra/lazy-collection": "^1.0" 55 | }, 56 | "require-dev": { 57 | "phpunit/phpunit": "^9.3", 58 | "php-mock/php-mock": "^2.2", 59 | "php-mock/php-mock-phpunit": "^2.6" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://www.slim-vue-app.ninja", 3 | "testFiles": "**/*.js", 4 | "fixturesFolder": "dev/js/e2e/fixtures", 5 | "integrationFolder": "dev/js/e2e/tests", 6 | "screenshotsFolder": "dev/js/e2e/screenshots", 7 | "videosFolder": "dev/js/e2e/videos", 8 | "pluginsFile": "dev/js/e2e/plugins", 9 | "supportFile": "dev/js/e2e/support" 10 | } -------------------------------------------------------------------------------- /db/migrations/20190119184642_create_users.sql: -------------------------------------------------------------------------------- 1 | -- migrate:up 2 | CREATE TABLE users( 3 | id int PRIMARY KEY AUTO_INCREMENT, 4 | email varchar(255) UNIQUE NOT NULL, 5 | username varchar(60) UNIQUE NOT NULL, 6 | password varchar(60) NOT NULL, 7 | created_at timestamp DEFAULT current_timestamp, 8 | updated_at timestamp DEFAULT current_timestamp 9 | ); 10 | 11 | -- migrate:down 12 | DROP TABLE users; 13 | 14 | -------------------------------------------------------------------------------- /db/migrations/20190119184740_create_user_remember.sql: -------------------------------------------------------------------------------- 1 | -- migrate:up 2 | CREATE TABLE user_remember( 3 | id int PRIMARY KEY AUTO_INCREMENT, 4 | user_id int NOT NULL, 5 | remember_id varchar(255), 6 | remember_token varchar(255), 7 | created_at timestamp DEFAULT current_timestamp, 8 | updated_at timestamp, 9 | FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE 10 | ); 11 | -- migrate:down 12 | DROP TABLE user_remember; -------------------------------------------------------------------------------- /db/migrations/20190119184757_create_user_permissions.sql: -------------------------------------------------------------------------------- 1 | -- migrate:up 2 | CREATE TABLE permissions( 3 | id int PRIMARY KEY AUTO_INCREMENT, 4 | name varchar(255) UNIQUE, 5 | created_at timestamp DEFAULT current_timestamp, 6 | updated_at timestamp 7 | ); 8 | CREATE TABLE roles( 9 | id int PRIMARY KEY AUTO_INCREMENT, 10 | name varchar(255) UNIQUE, 11 | created_at timestamp DEFAULT current_timestamp, 12 | updated_at timestamp 13 | ); 14 | CREATE TABLE role_permissions( 15 | id int PRIMARY KEY AUTO_INCREMENT, 16 | role_id int NOT NULL, 17 | permission_id int NOT NULL, 18 | created_at timestamp DEFAULT current_timestamp, 19 | updated_at timestamp, 20 | FOREIGN KEY(role_id) REFERENCES roles(id) ON DELETE CASCADE, 21 | FOREIGN KEY(permission_id) REFERENCES permissions(id) ON DELETE CASCADE 22 | ); 23 | CREATE TABLE user_roles( 24 | id int PRIMARY KEY AUTO_INCREMENT, 25 | user_id int NOT NULL, 26 | role_id int NOT NULL, 27 | created_at timestamp DEFAULT current_timestamp, 28 | updated_at timestamp, 29 | FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, 30 | FOREIGN KEY(role_id) REFERENCES roles(id) ON DELETE CASCADE 31 | ); 32 | CREATE TABLE admins( 33 | id int PRIMARY KEY AUTO_INCREMENT, 34 | user_id int NOT NULL, 35 | created_at timestamp DEFAULT current_timestamp, 36 | updated_at timestamp, 37 | FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE 38 | ); 39 | -- migrate:down 40 | DROP TABLE admins; 41 | DROP TABLE user_roles; 42 | DROP TABLE role_permissions; 43 | DROP TABLE roles; 44 | DROP TABLE permissions; -------------------------------------------------------------------------------- /db/migrations/20201027162730_2fa.sql: -------------------------------------------------------------------------------- 1 | -- migrate:up 2 | CREATE TABLE 2fa( 3 | id int PRIMARY KEY AUTO_INCREMENT, 4 | user_id int UNIQUE NOT NULL, 5 | discriminant varchar(255) UNIQUE NOT NULL, 6 | latest_code varchar(255), 7 | FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE, 8 | created_at timestamp DEFAULT current_timestamp, 9 | updated_at timestamp 10 | ); 11 | 12 | -- migrate:down 13 | DROP TABLE 2fa; 14 | 15 | -------------------------------------------------------------------------------- /dev/.htaccess: -------------------------------------------------------------------------------- 1 | Deny from all -------------------------------------------------------------------------------- /dev/.routeLoaderGenerator.php: -------------------------------------------------------------------------------- 1 | generateLoaderFile($loaderURI); 20 | } 21 | 22 | 23 | //Call it 24 | generateRouteAutoloadFile("routes", "route_autoload.php"); -------------------------------------------------------------------------------- /dev/Actions/Action.php: -------------------------------------------------------------------------------- 1 | container = $container; 27 | $this->config = $this->container->get("config"); 28 | } 29 | 30 | public static function from(...$args) 31 | { 32 | return new static(...$args); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /dev/Actions/Cookies.php: -------------------------------------------------------------------------------- 1 | resp = $container->get(ResponseAction::class); 25 | } 26 | 27 | protected function upgrade(ResponseInterface $res): Response 28 | { 29 | return $this->resp->upgrade($res); 30 | } 31 | 32 | public function builder(string $name): SetCookie 33 | { 34 | return SetCookie::create($name); 35 | } 36 | 37 | public function set(Response $res, SetCookie $cookie): Response 38 | { 39 | return $this->upgrade(FigResponseCookies::set($res, $cookie)); 40 | } 41 | 42 | public function get(ServerRequestInterface $rq, string $name, ?string $default = null): Cookie 43 | { 44 | return FigRequestCookies::get($rq, $name, $default); 45 | } 46 | 47 | public function has(ServerRequestInterface $rq, string $name): bool 48 | { 49 | return $this->get($rq, $name)->getValue() !== null; 50 | } 51 | 52 | public function expire(Response $res, string $name): Response 53 | { 54 | return $this->upgrade(FigResponseCookies::expire($res, $name)); 55 | } 56 | 57 | public function remove(Response $res, string $name): Response 58 | { 59 | return $this->upgrade(FigResponseCookies::remove($res, $name)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dev/Actions/Csrf.php: -------------------------------------------------------------------------------- 1 | container->get("config")["csrf"]; 26 | 27 | $this->key = $config["key"]; 28 | $this->session = $this->container->get("session"); 29 | 30 | $this->random = $container->get(Random::class); 31 | $this->hash = $container->get(Hash::class); 32 | } 33 | 34 | public function sessionKey(): string 35 | { 36 | return $this->key; 37 | } 38 | 39 | public function formKey(): string 40 | { 41 | return $this->sessionKey(); 42 | } 43 | 44 | public function hasToken(): bool 45 | { 46 | return $this->session->exists($this->sessionKey()); 47 | } 48 | 49 | public function getToken(): string 50 | { 51 | $this->ensureHasToken(); 52 | return $this->session->get($this->sessionKey()); 53 | } 54 | 55 | public function generateNewToken(): string 56 | { 57 | $token = $this->hash->hash( 58 | $this->random->generateString() 59 | ); 60 | $this->session->set($this->sessionKey(), $token); 61 | return $token; 62 | } 63 | 64 | public function isValid(string $token): bool 65 | { 66 | $actualToken = $this->getToken(); 67 | return $this->hash->checkHash($token, $actualToken); 68 | } 69 | 70 | public function ensureHasToken(): void 71 | { 72 | if (!$this->hasToken()) 73 | $this->generateNewToken(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /dev/Actions/FileSystem.php: -------------------------------------------------------------------------------- 1 | container->get($adapterClass); 23 | return new \League\Flysystem\Filesystem($adapter); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dev/Actions/Flash.php: -------------------------------------------------------------------------------- 1 | flash = $container->get("flash"); 25 | 26 | $this->now = new class ($this->flash) 27 | { 28 | protected $flash; 29 | public function __construct(FlashMessages $flash) 30 | { 31 | $this->flash = $flash; 32 | } 33 | 34 | public function success(string $msg): void 35 | { 36 | $this->flash->addMessageNow(Flash::SUCCESS, $msg); 37 | } 38 | public function failure(string $msg): void 39 | { 40 | $this->flash->addMessageNow(Flash::FAILURE, $msg); 41 | } 42 | public function info(string $msg): void 43 | { 44 | $this->flash->addMessageNow(Flash::INFO, $msg); 45 | } 46 | }; 47 | } 48 | 49 | public function success(string $msg): void 50 | { 51 | $this->flash->addMessage(self::SUCCESS, $msg); 52 | } 53 | public function failure(string $msg): void 54 | { 55 | $this->flash->addMessage(self::FAILURE, $msg); 56 | } 57 | public function info(string $msg): void 58 | { 59 | $this->flash->addMessage(self::INFO, $msg); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /dev/Actions/Hash.php: -------------------------------------------------------------------------------- 1 | get("config")["hash"]; 15 | 16 | $this->algo = $config["algo"]; 17 | $this->cost = $config["cost"]; 18 | $this->alt = $config["alt_algo"]; 19 | } 20 | 21 | public function hashPassword(string $password): string 22 | { 23 | return password_hash($password, $this->algo, [ 24 | "cost" => $this->cost, 25 | ]); 26 | } 27 | 28 | public function checkPassword(string $password, string $hash): bool 29 | { 30 | return password_verify($password, $hash); 31 | } 32 | 33 | public function hash(string $input): string 34 | { 35 | return hash($this->alt, $input); 36 | } 37 | 38 | public function checkHash(string $knowHash, string $desiredHash): bool 39 | { 40 | return hash_equals($knowHash, $desiredHash); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dev/Actions/PostValidator.php: -------------------------------------------------------------------------------- 1 | post = $container->get("request")->getParsedBody(); 16 | } 17 | 18 | 19 | public function required(array $keys): bool 20 | { 21 | return !array_diff_key(array_flip($keys), $this->post); 22 | } 23 | 24 | public function getAll(array $cfg): array 25 | { 26 | $ret = []; 27 | foreach ($cfg as $key => $uconfig) { 28 | $default = ["type" => "string", "default" => ""]; 29 | $config = array_merge($default, $uconfig); 30 | 31 | $value = !array_key_exists($key, $this->post) 32 | ? $config["default"] 33 | : $this->post[$key]; 34 | 35 | switch ($config["type"]) { 36 | case "bool": 37 | $value = (bool)$value; 38 | break; 39 | 40 | case "int": 41 | $value = (int)$value; 42 | break; 43 | 44 | case "float": 45 | $value = (float)$value; 46 | break; 47 | 48 | default: 49 | $value = (string)$value; 50 | break; 51 | } 52 | 53 | $ret[] = $value; 54 | } 55 | return $ret; 56 | } 57 | 58 | public function getOrDefault(string $key, $default) 59 | { 60 | if (array_key_exists($key, $this->post)) 61 | return $this->post[$key]; 62 | return $default; 63 | } 64 | 65 | public function payload(): array 66 | { 67 | return $this->post; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /dev/Actions/Random.php: -------------------------------------------------------------------------------- 1 | container->get("config")["random"]; 25 | 26 | $this->length = $config["length"]; 27 | $this->alphabet = $config["alphabet"]; 28 | 29 | $factory = new GeneratorFactory(); 30 | $this->generator = $factory->getMediumStrengthGenerator(); 31 | } 32 | 33 | public function generateString(): string 34 | { 35 | if (empty($this->alphabet)) 36 | return $this->generator->generateString($this->length); 37 | 38 | return $this->generator->generateString($this->length, $this->alphabet); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /dev/Actions/Response.php: -------------------------------------------------------------------------------- 1 | getHeaders()); 18 | return new SlimResponse($res->getStatusCode(), $headers, $res->getBody()); 19 | } 20 | 21 | public function redirect(ResponseInterface $res, string $location): ResponseInterface{ 22 | return $res->withHeader("Location", $location); 23 | } 24 | 25 | public function redirectWith(ResponseInterface $res, string $location, int $status = Httpstatuscodes::HTTP_TEMPORARY_REDIRECT){ 26 | return $this->redirect($res->withStatus($status), $location); 27 | } 28 | 29 | public function redirectToRoute(ResponseInterface $res, string $route, array $params = [], array $qs = []){ 30 | $parser = $this->container->get(RouteParser::class); 31 | $url = $parser->urlFor($route, $params, $qs); 32 | return $this->redirect($res, $url); 33 | } 34 | 35 | public function withJSON(ResponseInterface $res, $data): ResponseInterface{ 36 | $json = json_encode($data); 37 | $res->getBody()->write($json); 38 | return $res->withHeader("Content-Type", "application/json"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /dev/Actions/TwoFactor.php: -------------------------------------------------------------------------------- 1 | issuer = $this->config["2fa.issuer"]; 56 | $this->algo = $this->config["2fa.algo"]; 57 | $this->digits = $this->config["2fa.digits"]; 58 | $this->period = $this->config["2fa.period"]; 59 | $this->labelField = $this->config["2fa.label_field"]; 60 | 61 | $class = $this->config["2fa.qr_provider"]; 62 | $this->qrProvider = new $class(); 63 | 64 | $this->otp = new TwoFactorAuth( 65 | $this->issuer, 66 | $this->digits, 67 | $this->period, 68 | $this->algo, 69 | $this->qrProvider 70 | ); 71 | } 72 | 73 | protected function requires2FA(User $user): bool{ 74 | return $user->requires2FA(); 75 | } 76 | 77 | protected function getSeed(User $user): string{ 78 | return $user->twoFactor->discriminant; 79 | } 80 | 81 | /** 82 | * Validate 2FA for the given user 83 | * @param User $user 84 | * @param string $userCode 85 | * @return bool 86 | */ 87 | public function validate(User $user, string $userCode): bool{ 88 | if(!$this->requires2FA($user)) 89 | return true; 90 | 91 | $seed = $this->getSeed($user); 92 | return $this->otp->verifyCode($seed, $userCode); 93 | } 94 | 95 | /** 96 | * Generate a new discriminant for 2FA 97 | * @return string 98 | * @throws \RobThree\Auth\TwoFactorAuthException 99 | */ 100 | public function newDiscriminant(): string{ 101 | return $this->otp->createSecret(); 102 | } 103 | 104 | /** 105 | * Generate the QRCode URI for the given user 106 | * @param User $user 107 | * @return string 108 | * @throws \RobThree\Auth\TwoFactorAuthException 109 | */ 110 | public function qrCode(User $user): string{ 111 | if(!$this->requires2FA($user)) 112 | return ""; 113 | 114 | $seed = $this->getSeed($user); 115 | return $this->otp->getQRCodeImageAsDataUri($user->{$this->labelField}, $seed); 116 | } 117 | 118 | /** 119 | * Get the secret key for the given user 120 | * @param User $user 121 | * @return string 122 | */ 123 | public function secret(User $user): string{ 124 | if(!$this->requires2FA($user)) 125 | return ""; 126 | 127 | return $this->getSeed($user); 128 | } 129 | 130 | 131 | /** 132 | * Enable 2FA for the given user 133 | * @param User $user 134 | * @throws \RobThree\Auth\TwoFactorAuthException 135 | */ 136 | public function enable2FA(User $user){ 137 | if($this->requires2FA($user)) 138 | return; 139 | 140 | $discr = $this->newDiscriminant(); 141 | // dd($discr); 142 | 143 | $tfa = new \App\Models\TwoFactor(); 144 | $tfa->discriminant = $discr; 145 | $tfa->latest_code = null; 146 | 147 | $user->twoFactor()->save($tfa); 148 | } 149 | 150 | /** 151 | * Disable 2FA for the given user 152 | * @param User $user 153 | */ 154 | public function disable2FA(User $user){ 155 | if(!$this->requires2FA($user)) 156 | return; 157 | 158 | $user->twoFactor()->delete(); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /dev/Controllers/AuthController.php: -------------------------------------------------------------------------------- 1 | auth = $container->get(Auth::class); 23 | } 24 | 25 | /** 26 | * GET /auth/login 27 | * 28 | * @param Response $response 29 | * @return Response 30 | * @throws \Twig\Error\LoaderError 31 | * @throws \Twig\Error\RuntimeError 32 | * @throws \Twig\Error\SyntaxError 33 | */ 34 | public function loginForm(Response $response){ 35 | return $this->view->render($response, "auth/login.twig"); 36 | } 37 | 38 | /** 39 | * GET /auth/register 40 | * 41 | * @param Response $response 42 | * @return Response 43 | * @throws \Twig\Error\LoaderError 44 | * @throws \Twig\Error\RuntimeError 45 | * @throws \Twig\Error\SyntaxError 46 | */ 47 | public function registerForm(Response $response){ 48 | return $this->view->render($response, "auth/register.twig"); 49 | } 50 | 51 | /** 52 | * POST /auth/login 53 | * 54 | * @param Request $request 55 | * @param Response $response 56 | * @return Response 57 | */ 58 | public function login(Request $request, Response $response){ 59 | $data = $request->getParsedBody(); 60 | //TODO: Validation 61 | //TODO: Form requests 62 | $username = $data["username"] ?? ""; 63 | $password = $data["password"] ?? ""; 64 | $remember = $data["remember"] ?? false; 65 | 66 | $res = $this->responseUtils->upgrade($response); 67 | 68 | [$newResponse, $user] = $this->auth->login( 69 | $res, 70 | $username, 71 | $password, 72 | $remember 73 | )->asArray(); 74 | 75 | if($user === null){ 76 | $this->flash->failure("Failed to login"); 77 | return $this->responseUtils->redirectToRoute($newResponse, "auth.login", [ 78 | "old" => compact( 79 | "username", 80 | "remember" 81 | ), 82 | ]); 83 | }else{ 84 | $this->flash->success("Successful login as {$username}"); 85 | return $this->redirectHome($newResponse); 86 | } 87 | } 88 | 89 | /** 90 | * POST /auth/login 91 | * 92 | * @param Request $request 93 | * @param Response $response 94 | * @return Response 95 | */ 96 | public function register(Request $request, Response $response){ 97 | $data = $request->getParsedBody(); 98 | $email = $data["email"] ?? ""; 99 | $username = $data["username"] ?? ""; 100 | $password = $data["password"] ?? ""; 101 | $remember = $data["remember"] ?? false; 102 | $res = $this->responseUtils->upgrade($response); 103 | 104 | [$newResponse, $user] = $this->auth->register($res, $email, $username, $password, $remember)->asArray(); 105 | 106 | if($user === null){ 107 | $this->flash->failure("Failed to register $username"); 108 | }else{ 109 | $this->flash->success("Successfully registered $username"); 110 | } 111 | 112 | 113 | return $this->redirectHome($newResponse, [ 114 | "old" => compact( 115 | "email", 116 | "username", 117 | "remember" 118 | ), 119 | ]); 120 | } 121 | 122 | 123 | /** 124 | * GET /auth/logout 125 | * 126 | * @param Response $response 127 | * @return Response 128 | */ 129 | public function logout(Response $response){ 130 | $res = $this->responseUtils->upgrade($response); 131 | $response = $this->auth->logout($res); 132 | return $this->redirectHome($response); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /dev/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | container = $container; 38 | $this->view = $container->get(Twig::class); 39 | $this->responseUtils = $container->get(Response::class); 40 | $this->flash = $container->get(Flash::class); 41 | } 42 | 43 | protected function redirectHome(ResponseInterface $res, array $params = [], array $qs = []){ 44 | return $this->responseUtils->redirectToRoute($res, "home", $params, $qs); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dev/Controllers/DemoController.php: -------------------------------------------------------------------------------- 1 | container->get(FileSystem::class); 30 | $json = $fs->for(Local::class)->read("demo.json"); 31 | 32 | return $this->view->render($response, "demo.twig", [ 33 | "phpver" => phpversion(), 34 | "json" => $json, 35 | ]); 36 | } 37 | 38 | /** 39 | * GET /user/{user_} 40 | * @param Request $request 41 | * @param Response $response 42 | * @param string $user 43 | * @return Response 44 | * @throws \Twig\Error\LoaderError 45 | * @throws \Twig\Error\RuntimeError 46 | * @throws \Twig\Error\SyntaxError 47 | */ 48 | public function user(Request $request, Response $response, string $user){ 49 | // $user is the {user} route parameter 50 | return $this->view->render($response, "demo2.twig", compact( 51 | "user" 52 | )); 53 | } 54 | 55 | /** 56 | * GET /vmb/{user} 57 | * @param Request $request 58 | * @param Response $response 59 | * @param User $user 60 | * @return Response 61 | * @throws \Twig\Error\LoaderError 62 | * @throws \Twig\Error\RuntimeError 63 | * @throws \Twig\Error\SyntaxError 64 | */ 65 | public function vmb(Request $request, Response $response, User $user){ 66 | return $this->view->render($response, "demo3.twig", compact( 67 | "user" 68 | )); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /dev/Controllers/TwoFactorController.php: -------------------------------------------------------------------------------- 1 | qrCode($user); 28 | $secret = $tfa->secret($user); 29 | 30 | return $this->view->render($response, "2fa.twig", compact( 31 | "user", 32 | "qrcode", 33 | "secret" 34 | )); 35 | } 36 | 37 | /** 38 | * GET /2fa/enable/{user} 39 | * @param Response $response 40 | * @param User $user 41 | * @param TwoFactor $tfa 42 | * @return Response 43 | * @throws \RobThree\Auth\TwoFactorAuthException 44 | */ 45 | public function enable2FA(Response $response, User $user, TwoFactor $tfa){ 46 | $tfa->enable2FA($user); 47 | return $this->responseUtils->redirectToRoute($response, "demo.2fa", [ 48 | "__user" => $user->username, 49 | ]); 50 | } 51 | 52 | /** 53 | * GET /2fa/disable/{user} 54 | * @param Response $response 55 | * @param User $user 56 | * @param TwoFactor $tfa 57 | * @return Response 58 | */ 59 | public function disable2FA(Response $response, User $user, TwoFactor $tfa){ 60 | $tfa->disable2FA($user); 61 | return $this->responseUtils->redirectToRoute($response, "demo.2fa", [ 62 | "__user" => $user->username, 63 | ]); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /dev/Exceptions/CsrfTokenMismatch.php: -------------------------------------------------------------------------------- 1 | auth->isAdmin(); 7 | } 8 | 9 | /*protected function redirectURL(): string { 10 | return $this->auth->isLoggedIn() 11 | ? parent::redirectURL() 12 | : $this->router->urlFor("login"); 13 | }*/ 14 | } 15 | -------------------------------------------------------------------------------- /dev/Filters/ComposedFilter.php: -------------------------------------------------------------------------------- 1 | lhs = $lhs; 14 | $this->rhs = $rhs; 15 | } 16 | 17 | protected function isAuthorized(): bool { 18 | return $this->lhsAuthorizes() && $this->rhsAuthorizes(); 19 | } 20 | 21 | protected function redirectURL(): string { 22 | return $this->lhsAuthorizes() 23 | ? $this->rhsURL() 24 | : $this->lhsURL(); 25 | } 26 | 27 | protected function redirectStatus(): int { 28 | return $this->lhsAuthorizes() 29 | ? $this->rhsStatus() 30 | : $this->lhsStatus(); 31 | } 32 | 33 | protected function lhsAuthorizes(): bool{ 34 | return $this->lhs->isAuthorized(); 35 | } 36 | 37 | protected function rhsAuthorizes(): bool{ 38 | return $this->rhs->isAuthorized(); 39 | } 40 | 41 | protected function lhsURL(): string{ 42 | return $this->lhs->redirectURL(); 43 | } 44 | 45 | protected function rhsURL(): string{ 46 | return $this->rhs->redirectURL(); 47 | } 48 | 49 | protected function lhsStatus(): int{ 50 | return $this->lhs->redirectStatus(); 51 | } 52 | 53 | protected function rhsStatus(): int{ 54 | return $this->rhs->redirectStatus(); 55 | } 56 | } 57 | 58 | /** 59 | * Compose a series of filters (left associative) 60 | * @param string[] $filterClasses - The class names of the filters to compose (will be resolved via the DI container) 61 | * @return ComposedFilter|Filter 62 | * @throws \DI\DependencyException 63 | * @throws \DI\NotFoundException 64 | */ 65 | function composeFilters(array $filterClasses){ 66 | $composedFilter = filter($filterClasses[0]); 67 | 68 | for($i = 1, $length = count($filterClasses) ; $i < $length ; ++$i){ 69 | $filter = filter($filterClasses[$i]); 70 | 71 | $composedFilter = $composedFilter->composeWith($filter); 72 | } 73 | 74 | return $composedFilter; 75 | } 76 | -------------------------------------------------------------------------------- /dev/Filters/Filter.php: -------------------------------------------------------------------------------- 1 | router = $c->get("router"); 31 | $this->auth = $c->get(Auth::class); 32 | } 33 | 34 | public static function from(...$args) 35 | { 36 | return new static(...$args); 37 | } 38 | 39 | public static function compose(string $lhs, string $rhs){ 40 | return composeFilters([$lhs, $rhs]); 41 | } 42 | 43 | protected abstract function isAuthorized(): bool; 44 | 45 | protected function redirectURL(): string 46 | { 47 | return $this->router->urlFor("home"); 48 | } 49 | 50 | protected function redirectStatus(): ?int 51 | { 52 | // return Httpstatuscodes::HTTP_FORBIDDEN; 53 | // return Httpstatuscodes::HTTP_TEMPORARY_REDIRECT; 54 | return null; 55 | } 56 | 57 | public function process(ServerRequestInterface $req, RequestHandlerInterface $handler): ResponseInterface 58 | { 59 | if (!$this->isAuthorized()) { 60 | $res = new Response(); 61 | $status = $this->redirectStatus(); 62 | 63 | if($status !== null) 64 | return $this->responseUtils->redirectWith( 65 | $res, 66 | $this->redirectURL(), 67 | $status 68 | ); 69 | else 70 | return $this->responseUtils->redirect( 71 | $res, 72 | $this->redirectURL() 73 | ); 74 | } 75 | 76 | return $handler->handle($req); 77 | } 78 | 79 | public function composeWith(Filter $rhs): ComposedFilter 80 | { 81 | return ComposedFilter::from($this->container, $this, $rhs); 82 | } 83 | } 84 | 85 | /** 86 | * Resolve a route filter 87 | * @param $filterClass - The classname of the filter 88 | * @return Filter 89 | * @throws \DI\DependencyException 90 | * @throws \DI\NotFoundException 91 | */ 92 | function filter($filterClass){ 93 | return resolve($filterClass); 94 | } 95 | -------------------------------------------------------------------------------- /dev/Filters/LogoutFilter.php: -------------------------------------------------------------------------------- 1 | auth->isLoggedIn(); 8 | } 9 | } -------------------------------------------------------------------------------- /dev/Filters/UserFilter.php: -------------------------------------------------------------------------------- 1 | auth->isLoggedIn(); 13 | } 14 | 15 | protected function redirectURL(): string 16 | { 17 | return $this->router->urlFor("auth.login"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /dev/Filters/VisitorFilter.php: -------------------------------------------------------------------------------- 1 | auth->isLoggedIn(); 8 | } 9 | } -------------------------------------------------------------------------------- /dev/Handlers/ExceptionHandler.php: -------------------------------------------------------------------------------- 1 | view = resolve(Twig::class); 34 | } 35 | 36 | protected function render(int $status, \Throwable $e){ 37 | $res = new Response($status); 38 | try { 39 | return $this->view->render($res, "errors/$status.twig", [ 40 | "exception" => $e, 41 | ]); 42 | } catch (\Throwable $e) { 43 | $res->getBody()->write("Error $status"); 44 | return $res; 45 | } 46 | } 47 | 48 | protected function respond(): ResponseInterface{ 49 | $e = $this->exception; 50 | 51 | if(AppEnv::dev()){ 52 | // Delegate to other middlewares in dev 53 | throw $e; 54 | }if($e instanceof HttpBadRequestException){ 55 | return $this->render(400, $e); 56 | }else if($e instanceof HttpUnauthorizedException){ 57 | return $this->render(401, $e); 58 | }else if($e instanceof HttpForbiddenException){ 59 | return $this->render(403, $e); 60 | }else if($e instanceof HttpNotFoundException){ 61 | return $this->render(404, $e); 62 | }else{ // HttpInternalServerErrorException and others 63 | return $this->render(500, $e); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /dev/Handlers/LegacyPhpErrorHandler.php: -------------------------------------------------------------------------------- 1 | request = $request; 48 | $this->exceptionHandler = $exceptionHandler; 49 | $this->displayErrorDetails = $displayErrorDetails; 50 | $this->logErrors = $logErrors; 51 | $this->logErrorDetails = $logErrorDetails; 52 | $this->logger = resolve(LoggerInterface::class); 53 | } 54 | 55 | public function __invoke(){ 56 | $e = error_get_last(); 57 | 58 | if($e){ 59 | $errfile = $e["file"]; 60 | $errline = $e["line"]; 61 | $errstr = $e["message"]; 62 | $errno = $e["type"]; 63 | 64 | $msg = "Error #$errno $errstr on line $errline in $errfile"; 65 | 66 | switch($errno){ 67 | case E_USER_ERROR: 68 | $this->logger->error($msg); 69 | break; 70 | 71 | case E_USER_WARNING: 72 | $this->logger->warning($msg); 73 | break; 74 | 75 | case E_USER_NOTICE: 76 | $this->logger->info($msg); 77 | break; 78 | 79 | case E_USER_DEPRECATED: 80 | $this->logger->debug($msg); 81 | break; 82 | 83 | default: 84 | // Unknown => critical failure 85 | $this->logger->critical($msg); 86 | break; 87 | } 88 | 89 | throw new LegacyPhpError($errstr, $errno); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /dev/Helpers/AppEnv.php: -------------------------------------------------------------------------------- 1 | container = $container; 28 | $this->auth = $container->get(Auth::class); 29 | } 30 | 31 | public function getGlobals(): array 32 | { 33 | return [ 34 | "user" => $this->auth->user(), 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /dev/Helpers/TwigExtensions/CsrfExtension.php: -------------------------------------------------------------------------------- 1 | container = $container; 21 | } 22 | 23 | public function getFunctions() 24 | { 25 | return [ 26 | new TwigFunction("csrf_input", [$this, "csrfInput"], [ 27 | "is_safe" => ["html"] 28 | ]), 29 | ]; 30 | } 31 | 32 | protected function key(): string 33 | { 34 | return $this->container->get("view")[self::KEY]; 35 | } 36 | 37 | protected function token(): string 38 | { 39 | return $this->container->get("view")[self::TOKEN]; 40 | } 41 | 42 | public function csrfInput(): string 43 | { 44 | $key = $this->key(); 45 | $token = $this->token(); 46 | return ""; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /dev/Helpers/TwigExtensions/FlashExtension.php: -------------------------------------------------------------------------------- 1 | flash = $flash; 21 | } 22 | 23 | /** 24 | * Extension name. 25 | * 26 | * @return string 27 | */ 28 | public function getName() 29 | { 30 | return "slim-twig-flash"; 31 | } 32 | 33 | /** 34 | * Callback for twig. 35 | * 36 | * @return array 37 | */ 38 | public function getFunctions() 39 | { 40 | return [ 41 | new TwigFunction("flash", [$this, "getMessages"]), 42 | new TwigFunction("hasFlash", [$this, "hasMessages"]), 43 | ]; 44 | } 45 | 46 | public function hasMessages($key){ 47 | return $this->flash->hasMessage($key); 48 | } 49 | 50 | /** 51 | * Returns Flash messages; If key is provided then returns messages 52 | * for that key. 53 | * @param string|null $key 54 | * @return array 55 | */ 56 | public function getMessages($key = null) 57 | { 58 | if (null !== $key) { 59 | return $this->flash->getMessage($key); 60 | } 61 | 62 | return $this->flash->getMessages(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /dev/Helpers/TwigExtensions/PathExtension.php: -------------------------------------------------------------------------------- 1 | manifest = $container->get("manifest"); 28 | } 29 | 30 | public function getFunctions() 31 | { 32 | return [ 33 | new TwigFunction("partial", [$this, "partial"]), 34 | new TwigFunction("layout", [$this, "layout"]), 35 | new TwigFunction("module", [$this, "module"]), 36 | new TwigFunction("fromManifest", [$this, "fromManifest"]), 37 | new TwigFunction("manifest", [$this, "fromManifest"]), 38 | ]; 39 | } 40 | 41 | public function partial(string $path): string 42 | { 43 | return ".partials/{$path}.twig"; 44 | } 45 | 46 | public function layout(): string 47 | { 48 | return $this->partial("layout"); 49 | } 50 | 51 | public function module(string $path): string 52 | { 53 | return $this->partial("modules/{$path}"); 54 | } 55 | 56 | public function fromManifest(string $entry): string{ 57 | return $this->manifest[$entry] ?? ""; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /dev/Helpers/UserResponsePair.php: -------------------------------------------------------------------------------- 1 | response = $res; 20 | $this->user = $user; 21 | } 22 | 23 | public function asArray(): array 24 | { 25 | return [$this->response, $this->user]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /dev/Helpers/WhiteboxFeatures/AbstractDirectoryBrowser.php: -------------------------------------------------------------------------------- 1 | uri = $uri; 44 | } 45 | 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////// 49 | //Overrides 50 | ///////////////////////////////////////////////////////////////////////// 51 | /**Converts the instance to an array 52 | * @return array 53 | */ 54 | public function toArray(): array{ 55 | return iterator_to_array($this->getIterator()); 56 | } 57 | 58 | /**Converts the instance to a MagicalArray 59 | * @return MagicalArray 60 | */ 61 | public function toMagicalArray(): MagicalArray{ 62 | return new MagicalArray( 63 | $this->toArray() 64 | ); 65 | } 66 | } -------------------------------------------------------------------------------- /dev/Helpers/WhiteboxFeatures/ArrayHelper.php: -------------------------------------------------------------------------------- 1 | Iterator or 45 | * Traversable 46 | * @since 5.0.0 47 | */ 48 | public function getIterator(): Traversable{ 49 | $dir = new RecursiveDirectoryIterator($this->uri); 50 | return new RecursiveIteratorIterator($dir); 51 | } 52 | } -------------------------------------------------------------------------------- /dev/Helpers/WhiteboxFeatures/RouteLoader.php: -------------------------------------------------------------------------------- 1 | getPhpFiles(); 37 | 38 | $autoloader = fopen($autoloaderFileURI,"w+"); 39 | if(!$autoloader) 40 | throw new Exception("failed to create the autoloader file for the route loader."); 41 | 42 | fwrite($autoloader, "forEach(function(string $filePath) use($autoloader){ 44 | fwrite($autoloader, "require_once '{$filePath}';\n"); 45 | }); 46 | fclose($autoloader); 47 | 48 | return (string)realpath($autoloaderFileURI); 49 | } 50 | 51 | /**Retrieves the PHP files path from the directory of this loader 52 | * @return MagicalArray 53 | */ 54 | protected function getPhpFiles(): MagicalArray{ 55 | return (new RecursiveDirectoryBrowser($this->path)) 56 | ->toMagicalArray() 57 | ->filter(function(string $elem){ 58 | ["extension" => $extension] = pathinfo($elem); 59 | $isPhp = $extension === "php"; 60 | return $isPhp; 61 | }) 62 | ->sortBy(function(string $lhs, string $rhs): int{ 63 | if($lhs === $rhs) 64 | return 0; 65 | else{ 66 | $depthOf = function(string $str): int{ 67 | $str = str_replace("\\", "/", $str); 68 | $d = substr_count($str, "/"); 69 | if($d === false) 70 | return 1; 71 | return $d; 72 | }; 73 | 74 | $depthLhs = call_user_func($depthOf, $lhs); 75 | $depthRhs = call_user_func($depthOf, $rhs); 76 | 77 | if($depthLhs === $depthRhs) 78 | return $lhs <=> $rhs; 79 | else 80 | return $depthLhs <=> $depthRhs; 81 | } 82 | }); 83 | } 84 | } -------------------------------------------------------------------------------- /dev/Helpers/WhiteboxFeatures/T_RouteLoader.php: -------------------------------------------------------------------------------- 1 | path = $path; 39 | } 40 | 41 | 42 | 43 | ///////////////////////////////////////////////////////////////////////// 44 | //Methods 45 | ///////////////////////////////////////////////////////////////////////// 46 | /** 47 | * @param string $fileURI 48 | * @return string 49 | */ 50 | public abstract function generateLoaderFile(string $fileURI): string; 51 | } -------------------------------------------------------------------------------- /dev/Middlewares/AcceptsRedirect.php: -------------------------------------------------------------------------------- 1 | attr = $this->config["redirect.attribute"]; 23 | } 24 | 25 | public function process(Request $req, RequestHandlerInterface $handler): ResponseInterface 26 | { 27 | $request = $req->withAttribute($this->attr, true); 28 | return $handler->handle($req); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dev/Middlewares/Auth.php: -------------------------------------------------------------------------------- 1 | auth = $auth ?? $container->get(AuthAction::class); 21 | } 22 | 23 | 24 | public function process(Request $req, RequestHandlerInterface $handler): ResponseInterface 25 | { 26 | //TODO: Check if this still works after migrating to SlimV4 middlewares 27 | 28 | $rawResponse = $handler->handle($req); 29 | $response = $this->responseUtils->upgrade($rawResponse); 30 | return $this->auth->loginfromRemember($req, $response)->response; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dev/Middlewares/Csrf.php: -------------------------------------------------------------------------------- 1 | csrf = $container->get(CsrfAction::class); 23 | } 24 | 25 | public function process(Request $req, RequestHandlerInterface $handler): ResponseInterface 26 | { 27 | $this->csrf->ensureHasToken(); 28 | $key = $this->csrf->formKey(); 29 | 30 | if (in_array($req->getMethod(), static::METHODS)) { 31 | $params = $req->getParsedBody(); 32 | $submittedToken = $params[$key] ?? ""; 33 | 34 | if (!$this->csrf->isValid($submittedToken)) 35 | throw new CsrfTokenMismatch(); 36 | } 37 | 38 | $view = $this->container->get("view"); 39 | $view["csrf_key"] = $key; 40 | $view["csrf_token"] = $this->csrf->getToken(); 41 | 42 | $rawResponse = $handler->handle($req); 43 | return $this->responseUtils->upgrade($rawResponse); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /dev/Middlewares/Middleware.php: -------------------------------------------------------------------------------- 1 | process($req, $handler); 37 | } 38 | 39 | public abstract function process(Request $req, RequestHandlerInterface $handler): ResponseInterface; 40 | 41 | public static function from(...$args) 42 | { 43 | return new static(...$args); 44 | } 45 | 46 | public function __construct(ContainerInterface $container) 47 | { 48 | $this->container = $container; 49 | $this->responseUtils = $container->get(ResponseUpgrader::class); 50 | 51 | $this->config = $container->get(Config::class); 52 | $this->settings = $container->get("settings"); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /dev/Middlewares/RedirectAfterRequest.php: -------------------------------------------------------------------------------- 1 | mode = $this->config["redirect.mode"]; 37 | $this->key = $this->config["redirect.key"]; 38 | $this->attr = $this->config["redirect.attribute"]; 39 | $this->responseUtils = $container->get(Response::class); 40 | 41 | if(!in_array($this->mode, self::MODES)) 42 | throw new \InvalidArgumentException("Invalid mode for Redirectable"); 43 | } 44 | 45 | public function shouldRedirect(Request $req): bool{ 46 | return !!$req->getAttribute($this->attr); 47 | } 48 | 49 | public function process(Request $req, RequestHandlerInterface $handler): ResponseInterface 50 | { 51 | $url = null; 52 | 53 | try{ 54 | if($this->mode === "qs") 55 | $url = $this->processQueryString($req, $handler); 56 | else if($this->mode === "body") 57 | $url = $this->processBody($req, $handler); 58 | }catch(\Throwable $e){} 59 | 60 | $res = $handler->handle($req); 61 | return $url !== null ? $this->responseUtils->redirect($res, $url) : $res; 62 | } 63 | 64 | protected function processQueryString(Request $req, RequestHandlerInterface $handler) 65 | { 66 | return $req->getQueryParams()[$this->key]; 67 | //TODO: Make sure it's a valid URL? 68 | /*$res = $handler->handle($req); 69 | return $this->responseUtils->redirect($res, $url);*/ 70 | } 71 | 72 | protected function processBody(Request $req, RequestHandlerInterface $handler) 73 | { 74 | return $req->getParsedBody()[$this->key]; 75 | //TODO: Make sure it's a valid URL? 76 | /*$res = $handler->handle($req); 77 | return $this->responseUtils->redirect($res, $url);*/ 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /dev/Middlewares/RequestBinding.php: -------------------------------------------------------------------------------- 1 | container->set("request", $req); 16 | $this->container->set(ServerRequestInterface::class, $req); 17 | $res = $handler->handle($req); 18 | return $this->responseUtils->upgrade($res); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dev/Middlewares/Requires2FA.php: -------------------------------------------------------------------------------- 1 | auth = $container->get(\App\Actions\Auth::class); 43 | 44 | $this->usernameKey = $keys["username"] ?? "username"; 45 | $this->codeKey = $keys["code"] ?? "2fa"; 46 | } 47 | 48 | /** 49 | * @param Request $req 50 | * @param RequestHandlerInterface $handler 51 | * @return ResponseInterface 52 | * @throws Invalid2FA 53 | */ 54 | public function process(Request $req, RequestHandlerInterface $handler): ResponseInterface 55 | { 56 | $body = $req->getParsedBody(); 57 | $res = !$this->auth->isLoggedIn() 58 | ? $this->whenNotLoggedIn($req, $body) 59 | : $this->whenLoggedIn($req, $body); 60 | 61 | return $res ?? $handler->handle($req); 62 | } 63 | 64 | /** 65 | * @param Request $req 66 | * @param array|null|object $body 67 | * @return ResponseInterface|null 68 | * @throws Invalid2FA 69 | */ 70 | protected function whenNotLoggedIn(Request $req, $body): ?ResponseInterface 71 | { 72 | //TODO: See if we should prefer a legit login (w/ all parameters) instead of a forced on 73 | $username = $body[$this->usernameKey] ?? ""; 74 | $user = $this->auth->forceLogin(new Response(), $username)->user; 75 | $this->auth->logout(new Response()); 76 | 77 | return $user !== null ? $this->handleForUser($user, $body) : null; 78 | } 79 | 80 | /** 81 | * @param Request $req 82 | * @param array|null|object $body 83 | * @return ResponseInterface|null 84 | * @throws Invalid2FA 85 | */ 86 | protected function whenLoggedIn(Request $req, $body): ?ResponseInterface 87 | { 88 | $user = $this->auth->user(); 89 | return $this->handleForUser($user, $body); 90 | } 91 | 92 | /** 93 | * @param User $user 94 | * @param array|null|object $body 95 | * @return ResponseInterface|null 96 | * @throws Invalid2FA 97 | */ 98 | protected function handleForUser(User $user, $body): ?ResponseInterface{ 99 | $code = $body[$this->codeKey] ?? ""; 100 | $isValid = $this->auth->handle2FA($user, $code); 101 | 102 | if(!$isValid) 103 | throw new Invalid2FA(); 104 | 105 | return null; 106 | } 107 | } 108 | 109 | /** 110 | * Utility to create a 2FA middleware 111 | * @param array $keys 112 | * @return Requires2FA 113 | * @throws \DI\DependencyException 114 | * @throws \DI\NotFoundException 115 | */ 116 | function requires2FA(array $keys = []){ 117 | $actualKeys = array_merge([], [ 118 | "username" => "username", 119 | "code" => "2fa", 120 | ], $keys); 121 | 122 | /** 123 | * @var ContainerInterface $container 124 | */ 125 | $container = resolve(ContainerInterface::class); 126 | 127 | return new Requires2FA($container, $actualKeys); 128 | } 129 | -------------------------------------------------------------------------------- /dev/Middlewares/RouteModelBinding.php: -------------------------------------------------------------------------------- 1 | getRoute(); 28 | 29 | if(is_null($route)) 30 | throw new HttpNotFoundException($req); 31 | 32 | $parameterMapping = $this->config->get("viewModelBinding", []); 33 | collect($parameterMapping) 34 | ->lazy() 35 | ->filter(function ($mapping, $parameter) use($route){ 36 | return !is_null($route->getArgument($parameter)); 37 | })->each(function($mapping, $parameter) use($route, $req){ 38 | $discriminantValue = $route->getArgument($parameter); 39 | $modelClass = $mapping["model"]; 40 | $discriminantKey = $mapping["column"]; 41 | 42 | /** 43 | * @var Model|null $model 44 | */ 45 | $model = $modelClass::firstWhere($discriminantKey, $discriminantValue); 46 | if(is_null($model)) 47 | throw new HttpNotFoundException($req); 48 | 49 | $route->setArgument($parameter, $model); //TODO: Fix the fact that it converts to string 50 | $this->container->set($modelClass, $model); //WARNING: Dangerous fix w/ PHP-DI 51 | 52 | $route->setArgument("{$parameter}Discriminant", $discriminantValue); 53 | $route->setArgument("{$parameter}Id", $model->id); 54 | })->toArray(); 55 | 56 | return $handler->handle($req); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /dev/Models/Admin.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 12 | } 13 | } -------------------------------------------------------------------------------- /dev/Models/Permission.php: -------------------------------------------------------------------------------- 1 | hasManyThrough( 12 | Role::class, RolePermission::class, 13 | "permission_id", "id", 14 | "id", "role_id" 15 | );*/ 16 | return $this->belongsToMany(Role::class) 17 | ->using(RolePermission::class) 18 | // ->as("grant") 19 | ->withTimestamps(); 20 | } 21 | } -------------------------------------------------------------------------------- /dev/Models/Role.php: -------------------------------------------------------------------------------- 1 | hasManyThrough( 12 | Permission::class,RolePermission::class, 13 | "role_id", "id", 14 | "id", "permission_id" 15 | );*/ 16 | return $this->belongsToMany(Permission::class) 17 | ->using(RolePermission::class) 18 | // ->as("grant") 19 | ->withTimestamps(); 20 | } 21 | 22 | public function users(){ 23 | /*return $this->hasManyThrough( 24 | User::class, UserRole::class, 25 | "role_id", "id", 26 | "id", "user_id" 27 | );*/ 28 | return $this->belongsToMany(User::class) 29 | ->using(UserRole::class) 30 | // ->as("grant") 31 | ->withTimestamps(); 32 | } 33 | } -------------------------------------------------------------------------------- /dev/Models/RolePermission.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dev/Models/User.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Role::class) 14 | ->using(UserRole::class) 15 | ->withTimestamps(); 16 | } 17 | 18 | public function permissions(){ 19 | return $this->roles()->with("permission") 20 | ->cursor() 21 | ->flatMap(function(Role $role){ 22 | return $role->permissions; 23 | })->uniqueStrict(function(Permission $permission){ 24 | return $permission->id; 25 | }); 26 | } 27 | 28 | public function admin(){ 29 | return $this->hasOne(Admin::class); 30 | } 31 | 32 | public function remember(){ 33 | return $this->hasOne(UserRemember::class); 34 | } 35 | 36 | public function twoFactor(){ 37 | return $this->hasOne(TwoFactor::class); 38 | } 39 | 40 | public function requires2FA(): bool{ 41 | return $this->twoFactor()->exists(); 42 | } 43 | 44 | public function isAdmin(): bool{ 45 | return !is_null($this->admin); 46 | } 47 | 48 | public function hasRemember(): bool{ 49 | return !is_null($this->remember); 50 | } 51 | 52 | public function createRemember(?string $id, ?string $token): bool{ 53 | $ret = false; 54 | if(!$this->hasRemember()) { 55 | $ret = !!UserRemember::make($this, $id, $token); 56 | $this->refresh(); 57 | } 58 | return $ret; 59 | } 60 | 61 | public function updateRemember(?string $id, ?string $token): bool{ 62 | if(!$this->hasRemember()) 63 | return $this->createRemember($id, $token); 64 | return $this->remember->updateCredentials($id, $token); 65 | } 66 | 67 | public function resetRemember(): bool{ 68 | if($this->hasRemember()) 69 | return $this->remember()->first()->reset(); 70 | return true; 71 | } 72 | 73 | /** 74 | * Find a user from its username 75 | * @param string $username 76 | * @return User|null 77 | */ 78 | public static function fromUsername(string $username): ?self{ 79 | return self::where("username", $username)->first(); 80 | } 81 | 82 | /** 83 | * Create a new user 84 | * @param string $email 85 | * @param string $username 86 | * @param string $passwordHash 87 | * @return User 88 | */ 89 | public static function make(string $email, string $username, string $passwordHash): self{ 90 | return self::create([ 91 | "email" => $email, 92 | "username" => $username, 93 | "password" => $passwordHash 94 | ]); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /dev/Models/UserRemember.php: -------------------------------------------------------------------------------- 1 | belongsTo(User::class); 12 | } 13 | 14 | public function rid(): ?string{ 15 | return $this->remember_id; 16 | } 17 | 18 | public function token(): ?string{ 19 | return $this->remember_token; 20 | } 21 | 22 | public function hasID(): bool{ 23 | return !is_null($this->rid()); 24 | } 25 | 26 | public function hasToken(): bool{ 27 | return !is_null($this->token()); 28 | } 29 | 30 | public function updateCredentials(?string $id, ?string $token): bool{ 31 | return $this->update([ 32 | "remember_id" => $id, 33 | "remember_token" => $token 34 | ]); 35 | } 36 | 37 | public function reset(): bool{ 38 | return $this->updateCredentials(null, null); 39 | } 40 | 41 | public function scopeFromRID(/*Builder*/ $query, string $rid): ?self{ 42 | return $query->where("remember_id", $rid)->first(); 43 | } 44 | 45 | public static function make(User $user, ?string $id, ?string $token): ?self{ 46 | return self::create([ 47 | "user_id" => $user->id, 48 | "remember_id" => $id, 49 | "remember_token" => $token 50 | ]); 51 | } 52 | } -------------------------------------------------------------------------------- /dev/Models/UserRole.php: -------------------------------------------------------------------------------- 1 | get("config"); 7 | $settings = $container->get("settings"); 8 | $setupDb = require_once("db.php"); 9 | $db = $setupDb($config); 10 | 11 | $app = \DI\Bridge\Slim\Bridge::create($container); 12 | $container->set(\Slim\App::class, $app); 13 | 14 | $applyMiddlewares = require_once("middlewares.php"); 15 | $applyMiddlewares($app, $container, $config, $settings); 16 | 17 | require_once "route_autoload.php"; 18 | 19 | return $app; 20 | -------------------------------------------------------------------------------- /dev/config.php: -------------------------------------------------------------------------------- 1 | false, 14 | "settings" => [ 15 | "name" => "", 16 | "debug" => false, 17 | "displayErrorDetails" => false, 18 | "logErrors" => true, 19 | "logErrorDetails" => false, 20 | ], 21 | "logs" => [ 22 | "name" => "slim", 23 | "path" => "/logs/slim.log", // relative to the project root path 24 | "level" => Logger::ERROR, 25 | ], 26 | "views" => [ 27 | "cache" => sys_get_temp_dir() . "/views", // OR Path::cache("views") 28 | ], 29 | "csrf" => [ 30 | "key" => "csrf_token" 31 | ], 32 | "random" => [ 33 | "length" => 60, 34 | "alphabet" => null 35 | ], 36 | "session" => [ 37 | "name" => "session", 38 | "autorefresh" => true, 39 | "lifetime" => "20 minutes", 40 | "samesite" => true, 41 | "httponly" => true, 42 | ], 43 | "hash" => [ 44 | "algo" => PASSWORD_BCRYPT, 45 | "cost" => PASSWORD_BCRYPT_DEFAULT_COST, 46 | "alt_algo" => "sha256", 47 | ], 48 | "auth" => [ 49 | "container" => "user", 50 | "session" => "auth", 51 | "remember_name" => "remember", 52 | "remember_exp" => "+2 week", 53 | "cookie_separator" => "<@>", 54 | ], 55 | "db" => [ 56 | "driver" => "mysql", 57 | "host" => "localhost", 58 | "port" => "3306", 59 | "database" => "", 60 | "username" => "", 61 | "password" => "", 62 | "charset" => "", 63 | "collation" => "", 64 | "prefix" => "", 65 | ], 66 | "viewModelBinding" => [], 67 | "redirect" => [ 68 | "mode" => "qs", 69 | "key" => "redir", 70 | "attribute" => "shouldRedirect", 71 | ], 72 | "2fa" => [ 73 | "issuer" => "", 74 | "algo" => "sha1", 75 | "digits" => 6, 76 | "period" => 30, 77 | "qr_provider" => \RobThree\Auth\Providers\Qr\ImageChartsQRCodeProvider::class, 78 | "label_field" => "email", 79 | ], 80 | ]; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /dev/config/development.php: -------------------------------------------------------------------------------- 1 | true, 8 | "settings" => [ 9 | "name" => "slim_vue_app", 10 | "debug" => true, 11 | "displayErrorDetails" => true, 12 | "logErrors" => true, 13 | "logErrorDetails" => true, 14 | ], 15 | "logs" => [ 16 | "name" => "slim", 17 | "path" => "/logs/slim.log", // relative to the project root path 18 | "level" => Logger::DEBUG, 19 | ], 20 | "views" => [ 21 | "cache" => false, 22 | "debug" => true, 23 | ], 24 | "csrf" => [ 25 | "key" => "csrf_token", 26 | ], 27 | "random" => [ 28 | "length" => 128, 29 | "alphabet" => null, 30 | ], 31 | "session" => [ 32 | "name" => "session", 33 | "autorefresh" => true, 34 | "lifetime" => "2 hours", 35 | "samesite" => true, 36 | "httponly" => true, 37 | ], 38 | "hash" => [ 39 | "algo" => PASSWORD_BCRYPT, 40 | "cost" => 12, 41 | "alt_algo" => "sha256", 42 | ], 43 | "auth" => [ 44 | "container" => "user", 45 | "session" => "user_id", 46 | "remember_name" => "user_remember", 47 | "remember_exp" => "+2 week", 48 | "cookie_separator" => "____", 49 | ], 50 | "db" => [ 51 | "driver" => "mysql", 52 | "host" => "localhost", 53 | "port" => "3306", 54 | "database" => "slim_vue_app", 55 | "username" => "root", 56 | "password" => "", 57 | "charset" => "utf8", 58 | "collation" => "utf8_unicode_ci", 59 | "prefix" => "", 60 | ], 61 | "viewModelBinding" => [ 62 | "__user" => [ 63 | "model" => \App\Models\User::class, 64 | "column" => "username", 65 | ], 66 | ], 67 | "redirect" => [ 68 | "mode" => "qs", 69 | "key" => "redir", 70 | "attribute" => "shouldRedirect", 71 | ], 72 | "2fa" => [ 73 | "issuer" => "slim-vue-app.ninja", 74 | "algo" => "sha1", 75 | "digits" => 6, 76 | "period" => 30, 77 | "qr_provider" => \RobThree\Auth\Providers\Qr\ImageChartsQRCodeProvider::class, 78 | "label_field" => "username", 79 | ], 80 | ]; 81 | -------------------------------------------------------------------------------- /dev/config/production.php: -------------------------------------------------------------------------------- 1 | useAutowiring(true) 10 | ->useAnnotations(true) 11 | ->addDefinitions(Path::dev("/container/definitions.php")); 12 | 13 | $container = $builder->build(); 14 | 15 | /** 16 | * Resolve a dependency using the DI container 17 | * @param class-string|string $key 18 | * @return mixed 19 | * @throws \DI\DependencyException 20 | * @throws \DI\NotFoundException 21 | */ 22 | function resolve($key){ 23 | global $container; 24 | return $container->get($key); 25 | } 26 | 27 | function controllerMethod(string $controllerClass, string $method){ 28 | return \App\Helpers\Routing::controllerMethod($controllerClass, $method); 29 | } 30 | 31 | function cm(string $controllerClass, string $method){ 32 | return controllerMethod($controllerClass, $method); 33 | } 34 | 35 | return $container; 36 | -------------------------------------------------------------------------------- /dev/db.php: -------------------------------------------------------------------------------- 1 | addConnection($config["db"]); 8 | $db->setAsGlobal(); 9 | $db->bootEloquent(); 10 | 11 | return $db; 12 | }; 13 | -------------------------------------------------------------------------------- /dev/dumpRouteLoader.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | BASEDIR=$(dirname "$0") 3 | php=`type -P php` 4 | $php -f ".routeLoaderGenerator.php" 5 | 6 | LCYAN='\033[1;36m' 7 | NC='\033[0m' 8 | 9 | echo -e "${LCYAN}route autoloader generated${NC}"; -------------------------------------------------------------------------------- /dev/env.php: -------------------------------------------------------------------------------- 1 | load(); 8 | -------------------------------------------------------------------------------- /dev/js/2fa.js: -------------------------------------------------------------------------------- 1 | import { newVueInstance } from "@/vue"; 2 | import router from "@/vue/router"; 3 | import store from "@/vue/store"; 4 | import { browserDetectorOf } from "@js/utils/BrowserDetector"; 5 | import QrCode from "@components/QrCode"; 6 | 7 | (() => { 8 | console.log( 9 | "%c VOLTRA ", 10 | `background: #0087b3; 11 | color: white; 12 | font-size: 17px; 13 | font-weight: bold; 14 | line-height: 36px; 15 | text-align: center; 16 | border-radius: 50vw;`, 17 | "www.ludwigguerin.fr" 18 | ); 19 | 20 | const browserDetector = browserDetectorOf(window).bootstrapClasses(); 21 | 22 | const vm = newVueInstance({ 23 | el: "#app", 24 | router, 25 | store, 26 | components: { QrCode }, 27 | }); 28 | })(); 29 | -------------------------------------------------------------------------------- /dev/js/demo.js: -------------------------------------------------------------------------------- 1 | const add = (a, b) => a + b; 2 | 3 | export { add }; 4 | -------------------------------------------------------------------------------- /dev/js/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | env: { 5 | node: true, 6 | browser: false, 7 | }, 8 | rules: { "no-undef": "off" }, 9 | }; 10 | -------------------------------------------------------------------------------- /dev/js/e2e/fixtures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/js/e2e/fixtures/.gitkeep -------------------------------------------------------------------------------- /dev/js/e2e/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /dev/js/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | const webpackPreprocessor = require("@cypress/webpack-preprocessor"); 16 | 17 | /** 18 | * @type {Cypress.PluginConfig} 19 | */ 20 | module.exports = (on, config) => { 21 | // `on` is used to hook into various events Cypress emits 22 | // `config` is the resolved Cypress config 23 | 24 | 25 | /** 26 | * @var {Cypress.PreprocessorOptions} options 27 | */ 28 | const options = { 29 | webpackOptions: require("../../../../webpack.config"), 30 | watchOptions: {} 31 | }; 32 | 33 | on("file:preprocessor", webpackPreprocessor(options)); 34 | }; 35 | -------------------------------------------------------------------------------- /dev/js/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /dev/js/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/demo.e2e.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | /* eslint-disable no-undef */ 3 | 4 | describe("My Demo Test", () => { 5 | it("Does stuff I guess", () => { 6 | expect(true).to.eq(true); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/aliasing.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Aliasing", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/aliasing"); 6 | }); 7 | 8 | it(".as() - alias a DOM element for later use", () => { 9 | // https://on.cypress.io/as 10 | 11 | // Alias a DOM element for use later 12 | // We don't have to traverse to the element 13 | // later in our code, we reference it with @ 14 | 15 | cy.get(".as-table").find("tbody>tr") 16 | .first() 17 | .find("td") 18 | .first() 19 | .find("button") 20 | .as("firstBtn"); 21 | 22 | // when we reference the alias, we place an 23 | // @ in front of its name 24 | cy.get("@firstBtn").click(); 25 | 26 | cy.get("@firstBtn") 27 | .should("have.class", "btn-success") 28 | .and("contain", "Changed"); 29 | }); 30 | 31 | it(".as() - alias a route for later use", () => { 32 | // Alias the route to wait for its response 33 | cy.server(); 34 | cy.route("GET", "comments/*").as("getComment"); 35 | 36 | // we have code that gets a comment when 37 | // the button is clicked in scripts.js 38 | cy.get(".network-btn").click(); 39 | 40 | // https://on.cypress.io/wait 41 | cy.wait("@getComment").its("status") 42 | .should("eq", 200); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/assertions.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Assertions", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/assertions"); 6 | }); 7 | 8 | describe("Implicit Assertions", () => { 9 | it(".should() - make an assertion about the current subject", () => { 10 | // https://on.cypress.io/should 11 | cy.get(".assertion-table") 12 | .find("tbody tr:last") 13 | .should("have.class", "success") 14 | .find("td") 15 | .first() 16 | // checking the text of the element in various ways 17 | .should("have.text", "Column content") 18 | .should("contain", "Column content") 19 | .should("have.html", "Column content") 20 | // chai-jquery uses "is()" to check if element matches selector 21 | .should("match", "td") 22 | // to match text content against a regular expression 23 | // first need to invoke jQuery method text() 24 | // and then match using regular expression 25 | .invoke("text") 26 | .should("match", /column content/i); 27 | 28 | // a better way to check element's text content against a regular expression 29 | // is to use "cy.contains" 30 | // https://on.cypress.io/contains 31 | cy.get(".assertion-table") 32 | .find("tbody tr:last") 33 | // finds first element with text content matching regular expression 34 | .contains("td", /column content/i) 35 | .should("be.visible"); 36 | 37 | // for more information about asserting element's text 38 | // see https://on.cypress.io/using-cypress-faq#How-do-I-get-an-element’s-text-contents 39 | }); 40 | 41 | it(".and() - chain multiple assertions together", () => { 42 | // https://on.cypress.io/and 43 | cy.get(".assertions-link") 44 | .should("have.class", "active") 45 | .and("have.attr", "href") 46 | .and("include", "cypress.io"); 47 | }); 48 | }); 49 | 50 | describe("Explicit Assertions", () => { 51 | // https://on.cypress.io/assertions 52 | it("expect - make an assertion about a specified subject", () => { 53 | // We can use Chai's BDD style assertions 54 | expect(true).to.be.true; 55 | const o = { foo: "bar" }; 56 | 57 | expect(o).to.equal(o); 58 | expect(o).to.deep.equal({ foo: "bar" }); 59 | // matching text using regular expression 60 | expect("FooBar").to.match(/bar$/i); 61 | }); 62 | 63 | it("pass your own callback function to should()", () => { 64 | // Pass a function to should that can have any number 65 | // of explicit assertions within it. 66 | // The ".should(cb)" function will be retried 67 | // automatically until it passes all your explicit assertions or times out. 68 | cy.get(".assertions-p") 69 | .find("p") 70 | .should($p => { 71 | // https://on.cypress.io/$ 72 | // return an array of texts from all of the p's 73 | // @ts-ignore TS6133 unused variable 74 | const texts = $p.map((i, el) => Cypress.$(el).text()); 75 | 76 | // jquery map returns jquery object 77 | // and .get() convert this to simple array 78 | const paragraphs = texts.get(); 79 | 80 | // array should have length of 3 81 | expect(paragraphs, "has 3 paragraphs").to.have.length(3); 82 | 83 | // use second argument to expect(...) to provide clear 84 | // message with each assertion 85 | expect(paragraphs, "has expected text in each paragraph").to.deep.eq([ 86 | "Some text from first p", 87 | "More text from second p", 88 | "And even more text from third p", 89 | ]); 90 | }); 91 | }); 92 | 93 | it("finds element by class name regex", () => { 94 | cy.get(".docs-header") 95 | .find("div") 96 | // .should(cb) callback function will be retried 97 | .should($div => { 98 | expect($div).to.have.length(1); 99 | 100 | const { className } = $div[0]; 101 | 102 | expect(className).to.match(/heading-/); 103 | }) 104 | // .then(cb) callback is not retried, 105 | // it either passes or fails 106 | .then($div => { 107 | expect($div, "text content").to.have.text("Introduction"); 108 | }); 109 | }); 110 | 111 | it("can throw any error", () => { 112 | cy.get(".docs-header") 113 | .find("div") 114 | .should($div => { 115 | if($div.length !== 1){ 116 | // you can throw your own errors 117 | throw new Error("Did not find 1 element"); 118 | } 119 | 120 | const { className } = $div[0]; 121 | 122 | if(!className.match(/heading-/)) 123 | throw new Error(`Could not find class "heading-" in ${className}`); 124 | }); 125 | }); 126 | 127 | it("matches unknown text between two elements", () => { 128 | /** 129 | * Text from the first element. 130 | * @type {string} 131 | */ 132 | let text; 133 | 134 | /** 135 | * Normalizes passed text, 136 | * useful before comparing text with spaces and different capitalization. 137 | * @param {string} s Text to normalize 138 | */ 139 | const normalizeText = s => s.replace(/\s/g, "").toLowerCase(); 140 | 141 | cy.get(".two-elements") 142 | .find(".first") 143 | .then($first => { 144 | // save text from the first element 145 | text = normalizeText($first.text()); 146 | }); 147 | 148 | cy.get(".two-elements") 149 | .find(".second") 150 | .should($div => { 151 | // we can massage text before comparing 152 | const secondText = normalizeText($div.text()); 153 | 154 | expect(secondText, "second text").to.equal(text); 155 | }); 156 | }); 157 | 158 | it("assert - assert shape of an object", () => { 159 | const person = { 160 | name: "Joe", 161 | age: 20, 162 | }; 163 | 164 | assert.isObject(person, "value is object"); 165 | }); 166 | 167 | it("retries the should callback until assertions pass", () => { 168 | cy.get("#random-number") 169 | .should($div => { 170 | const n = parseFloat($div.text()); 171 | 172 | expect(n).to.be.gte(1).and.be.lte(10); 173 | }); 174 | }); 175 | }); 176 | }); 177 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/connectors.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Connectors", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/connectors"); 6 | }); 7 | 8 | it(".each() - iterate over an array of elements", () => { 9 | // https://on.cypress.io/each 10 | cy.get(".connectors-each-ul>li") 11 | .each(( 12 | $el, index, $list 13 | ) => { 14 | console.log( 15 | $el, index, $list 16 | ); 17 | }); 18 | }); 19 | 20 | it(".its() - get properties on the current subject", () => { 21 | // https://on.cypress.io/its 22 | cy.get(".connectors-its-ul>li") 23 | // calls the 'length' property yielding that value 24 | .its("length") 25 | .should("be.gt", 2); 26 | }); 27 | 28 | it(".invoke() - invoke a function on the current subject", () => { 29 | // our div is hidden in our script.js 30 | // $('.connectors-div').hide() 31 | 32 | // https://on.cypress.io/invoke 33 | cy.get(".connectors-div").should("be.hidden") 34 | // call the jquery method 'show' on the 'div.container' 35 | .invoke("show") 36 | .should("be.visible"); 37 | }); 38 | 39 | it(".spread() - spread an array as individual args to callback function", () => { 40 | // https://on.cypress.io/spread 41 | const arr = [ 42 | "foo", "bar", "baz", 43 | ]; 44 | 45 | cy.wrap(arr).spread(( 46 | foo, bar, baz 47 | ) => { 48 | expect(foo).to.eq("foo"); 49 | expect(bar).to.eq("bar"); 50 | expect(baz).to.eq("baz"); 51 | }); 52 | }); 53 | 54 | describe(".then()", () => { 55 | it("invokes a callback function with the current subject", () => { 56 | // https://on.cypress.io/then 57 | cy.get(".connectors-list > li") 58 | .then($lis => { 59 | expect($lis, "3 items").to.have.length(3); 60 | expect($lis.eq(0), "first item").to.contain("Walk the dog"); 61 | expect($lis.eq(1), "second item").to.contain("Feed the cat"); 62 | expect($lis.eq(2), "third item").to.contain("Write JavaScript"); 63 | }); 64 | }); 65 | 66 | it("yields the returned value to the next command", () => { 67 | cy.wrap(1) 68 | .then(num => { 69 | expect(num).to.equal(1); 70 | 71 | return 2; 72 | }) 73 | .then(num => { 74 | expect(num).to.equal(2); 75 | }); 76 | }); 77 | 78 | it("yields the original subject without return", () => { 79 | cy.wrap(1) 80 | .then(num => { 81 | expect(num).to.equal(1); 82 | // note that nothing is returned from this callback 83 | }) 84 | .then(num => { 85 | // this callback receives the original unchanged value 1 86 | expect(num).to.equal(1); 87 | }); 88 | }); 89 | 90 | it("yields the value yielded by the last Cypress command inside", () => { 91 | cy.wrap(1) 92 | .then(num => { 93 | expect(num).to.equal(1); 94 | // note how we run a Cypress command 95 | // the result yielded by this Cypress command 96 | // will be passed to the second ".then" 97 | cy.wrap(2); 98 | }) 99 | .then(num => { 100 | // this callback receives the value yielded by "cy.wrap(2)" 101 | expect(num).to.equal(2); 102 | }); 103 | }); 104 | }); 105 | }); 106 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/cookies.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Cookies", () => { 4 | beforeEach(() => { 5 | Cypress.Cookies.debug(true); 6 | 7 | cy.visit("https://example.cypress.io/commands/cookies"); 8 | 9 | // clear cookies again after visiting to remove 10 | // any 3rd party cookies picked up such as cloudflare 11 | cy.clearCookies(); 12 | }); 13 | 14 | it("cy.getCookie() - get a browser cookie", () => { 15 | // https://on.cypress.io/getcookie 16 | cy.get("#getCookie .set-a-cookie").click(); 17 | 18 | // cy.getCookie() yields a cookie object 19 | cy.getCookie("token").should( 20 | "have.property", "value", "123ABC" 21 | ); 22 | }); 23 | 24 | it("cy.getCookies() - get browser cookies", () => { 25 | // https://on.cypress.io/getcookies 26 | cy.getCookies().should("be.empty"); 27 | 28 | cy.get("#getCookies .set-a-cookie").click(); 29 | 30 | // cy.getCookies() yields an array of cookies 31 | cy.getCookies().should("have.length", 1) 32 | .should(cookies => { 33 | // each cookie has these properties 34 | expect(cookies[0]).to.have.property("name", "token"); 35 | expect(cookies[0]).to.have.property("value", "123ABC"); 36 | expect(cookies[0]).to.have.property("httpOnly", false); 37 | expect(cookies[0]).to.have.property("secure", false); 38 | expect(cookies[0]).to.have.property("domain"); 39 | expect(cookies[0]).to.have.property("path"); 40 | }); 41 | }); 42 | 43 | it("cy.setCookie() - set a browser cookie", () => { 44 | // https://on.cypress.io/setcookie 45 | cy.getCookies().should("be.empty"); 46 | 47 | cy.setCookie("foo", "bar"); 48 | 49 | // cy.getCookie() yields a cookie object 50 | cy.getCookie("foo").should( 51 | "have.property", "value", "bar" 52 | ); 53 | }); 54 | 55 | it("cy.clearCookie() - clear a browser cookie", () => { 56 | // https://on.cypress.io/clearcookie 57 | cy.getCookie("token").should("be.null"); 58 | 59 | cy.get("#clearCookie .set-a-cookie").click(); 60 | 61 | cy.getCookie("token").should( 62 | "have.property", "value", "123ABC" 63 | ); 64 | 65 | // cy.clearCookies() yields null 66 | cy.clearCookie("token").should("be.null"); 67 | 68 | cy.getCookie("token").should("be.null"); 69 | }); 70 | 71 | it("cy.clearCookies() - clear browser cookies", () => { 72 | // https://on.cypress.io/clearcookies 73 | cy.getCookies().should("be.empty"); 74 | 75 | cy.get("#clearCookies .set-a-cookie").click(); 76 | 77 | cy.getCookies().should("have.length", 1); 78 | 79 | // cy.clearCookies() yields null 80 | cy.clearCookies(); 81 | 82 | cy.getCookies().should("be.empty"); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/files.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /// JSON fixture file can be loaded directly using 4 | // the built-in JavaScript bundler 5 | // @ts-ignore 6 | const requiredExample = require("../../../fixtures/example"); 7 | 8 | context("Files", () => { 9 | beforeEach(() => { 10 | cy.visit("https://example.cypress.io/commands/files"); 11 | }); 12 | 13 | beforeEach(() => { 14 | // load example.json fixture file and store 15 | // in the test context object 16 | cy.fixture("example.json").as("example"); 17 | }); 18 | 19 | it("cy.fixture() - load a fixture", () => { 20 | // https://on.cypress.io/fixture 21 | 22 | // Instead of writing a response inline you can 23 | // use a fixture file's content. 24 | 25 | cy.server(); 26 | cy.fixture("example.json").as("comment"); 27 | // when application makes an Ajax request matching "GET comments/*" 28 | // Cypress will intercept it and reply with object 29 | // from the "comment" alias 30 | cy.route( 31 | "GET", "comments/*", "@comment" 32 | ).as("getComment"); 33 | 34 | // we have code that gets a comment when 35 | // the button is clicked in scripts.js 36 | cy.get(".fixture-btn").click(); 37 | 38 | cy.wait("@getComment").its("responseBody") 39 | .should("have.property", "name") 40 | .and("include", "Using fixtures to represent data"); 41 | 42 | // you can also just write the fixture in the route 43 | cy.route( 44 | "GET", "comments/*", "fixture:example.json" 45 | ).as("getComment"); 46 | 47 | // we have code that gets a comment when 48 | // the button is clicked in scripts.js 49 | cy.get(".fixture-btn").click(); 50 | 51 | cy.wait("@getComment").its("responseBody") 52 | .should("have.property", "name") 53 | .and("include", "Using fixtures to represent data"); 54 | 55 | // or write fx to represent fixture 56 | // by default it assumes it's .json 57 | cy.route( 58 | "GET", "comments/*", "fx:example" 59 | ).as("getComment"); 60 | 61 | // we have code that gets a comment when 62 | // the button is clicked in scripts.js 63 | cy.get(".fixture-btn").click(); 64 | 65 | cy.wait("@getComment").its("responseBody") 66 | .should("have.property", "name") 67 | .and("include", "Using fixtures to represent data"); 68 | }); 69 | 70 | it("cy.fixture() or require - load a fixture", function(){ 71 | // we are inside the "function () { ... }" 72 | // callback and can use test context object "this" 73 | // "this.example" was loaded in "beforeEach" function callback 74 | expect(this.example, "fixture in the test context") 75 | .to.deep.equal(requiredExample); 76 | 77 | // or use "cy.wrap" and "should('deep.equal', ...)" assertion 78 | // @ts-ignore 79 | cy.wrap(this.example, "fixture vs require") 80 | .should("deep.equal", requiredExample); 81 | }); 82 | 83 | it("cy.readFile() - read file contents", () => { 84 | // https://on.cypress.io/readfile 85 | 86 | // You can read a file and yield its contents 87 | // The filePath is relative to your project's root. 88 | cy.readFile("cypress.json").then(json => { 89 | expect(json).to.be.an("object"); 90 | }); 91 | }); 92 | 93 | it("cy.writeFile() - write to a file", () => { 94 | // https://on.cypress.io/writefile 95 | 96 | // You can write to a file 97 | 98 | // Use a response from a request to automatically 99 | // generate a fixture file for use later 100 | cy.request("https://jsonplaceholder.cypress.io/users") 101 | .then(response => { 102 | cy.writeFile("cypress/fixtures/users.json", response.body); 103 | }); 104 | 105 | cy.fixture("users").should(users => { 106 | expect(users[0].name).to.exist; 107 | }); 108 | 109 | // JavaScript arrays and objects are stringified 110 | // and formatted into text. 111 | cy.writeFile("cypress/fixtures/profile.json", { 112 | id: 8739, 113 | name: "Jane", 114 | email: "jane@example.com", 115 | }); 116 | 117 | cy.fixture("profile").should(profile => { 118 | expect(profile.name).to.eq("Jane"); 119 | }); 120 | }); 121 | }); 122 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/local_storage.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Local Storage", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/local-storage"); 6 | }); 7 | // Although local storage is automatically cleared 8 | // in between tests to maintain a clean state 9 | // sometimes we need to clear the local storage manually 10 | 11 | it("cy.clearLocalStorage() - clear all data in local storage", () => { 12 | // https://on.cypress.io/clearlocalstorage 13 | cy.get(".ls-btn").click() 14 | .should(() => { 15 | expect(localStorage.getItem("prop1")).to.eq("red"); 16 | expect(localStorage.getItem("prop2")).to.eq("blue"); 17 | expect(localStorage.getItem("prop3")).to.eq("magenta"); 18 | }); 19 | 20 | // clearLocalStorage() yields the localStorage object 21 | cy.clearLocalStorage().should(ls => { 22 | expect(ls.getItem("prop1")).to.be.null; 23 | expect(ls.getItem("prop2")).to.be.null; 24 | expect(ls.getItem("prop3")).to.be.null; 25 | }); 26 | 27 | // Clear key matching string in Local Storage 28 | cy.get(".ls-btn").click() 29 | .should(() => { 30 | expect(localStorage.getItem("prop1")).to.eq("red"); 31 | expect(localStorage.getItem("prop2")).to.eq("blue"); 32 | expect(localStorage.getItem("prop3")).to.eq("magenta"); 33 | }); 34 | 35 | cy.clearLocalStorage("prop1").should(ls => { 36 | expect(ls.getItem("prop1")).to.be.null; 37 | expect(ls.getItem("prop2")).to.eq("blue"); 38 | expect(ls.getItem("prop3")).to.eq("magenta"); 39 | }); 40 | 41 | // Clear keys matching regex in Local Storage 42 | cy.get(".ls-btn").click() 43 | .should(() => { 44 | expect(localStorage.getItem("prop1")).to.eq("red"); 45 | expect(localStorage.getItem("prop2")).to.eq("blue"); 46 | expect(localStorage.getItem("prop3")).to.eq("magenta"); 47 | }); 48 | 49 | cy.clearLocalStorage(/prop1|2/).should(ls => { 50 | expect(ls.getItem("prop1")).to.be.null; 51 | expect(ls.getItem("prop2")).to.be.null; 52 | expect(ls.getItem("prop3")).to.eq("magenta"); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/location.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Location", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/location"); 6 | }); 7 | 8 | it("cy.hash() - get the current URL hash", () => { 9 | // https://on.cypress.io/hash 10 | cy.hash().should("be.empty"); 11 | }); 12 | 13 | it("cy.location() - get window.location", () => { 14 | // https://on.cypress.io/location 15 | cy.location().should(location => { 16 | expect(location.hash).to.be.empty; 17 | expect(location.href).to.eq("https://example.cypress.io/commands/location"); 18 | expect(location.host).to.eq("example.cypress.io"); 19 | expect(location.hostname).to.eq("example.cypress.io"); 20 | expect(location.origin).to.eq("https://example.cypress.io"); 21 | expect(location.pathname).to.eq("/commands/location"); 22 | expect(location.port).to.eq(""); 23 | expect(location.protocol).to.eq("https:"); 24 | expect(location.search).to.be.empty; 25 | }); 26 | }); 27 | 28 | it("cy.url() - get the current URL", () => { 29 | // https://on.cypress.io/url 30 | cy.url().should("eq", "https://example.cypress.io/commands/location"); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/misc.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Misc", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/misc"); 6 | }); 7 | 8 | it(".end() - end the command chain", () => { 9 | // https://on.cypress.io/end 10 | 11 | // cy.end is useful when you want to end a chain of commands 12 | // and force Cypress to re-query from the root element 13 | cy.get(".misc-table").within(() => { 14 | // ends the current chain and yields null 15 | cy.contains("Cheryl").click() 16 | .end(); 17 | 18 | // queries the entire table again 19 | cy.contains("Charles").click(); 20 | }); 21 | }); 22 | 23 | it("cy.exec() - execute a system command", () => { 24 | // execute a system command. 25 | // so you can take actions necessary for 26 | // your test outside the scope of Cypress. 27 | // https://on.cypress.io/exec 28 | 29 | // we can use Cypress.platform string to 30 | // select appropriate command 31 | // https://on.cypress/io/platform 32 | cy.log(`Platform ${Cypress.platform} architecture ${Cypress.arch}`); 33 | 34 | // on CircleCI Windows build machines we have a failure to run bash shell 35 | // https://github.com/cypress-io/cypress/issues/5169 36 | // so skip some of the tests by passing flag "--env circle=true" 37 | const isCircleOnWindows = Cypress.platform === "win32" && Cypress.env("circle"); 38 | 39 | if(isCircleOnWindows){ 40 | cy.log("Skipping test on CircleCI"); 41 | 42 | return; 43 | } 44 | 45 | // cy.exec problem on Shippable CI 46 | // https://github.com/cypress-io/cypress/issues/6718 47 | const isShippable = Cypress.platform === "linux" && Cypress.env("shippable"); 48 | 49 | if(isShippable){ 50 | cy.log("Skipping test on ShippableCI"); 51 | 52 | return; 53 | } 54 | 55 | cy.exec("echo Jane Lane") 56 | .its("stdout") 57 | .should("contain", "Jane Lane"); 58 | 59 | if(Cypress.platform === "win32"){ 60 | cy.exec("print cypress.json") 61 | .its("stderr") 62 | .should("be.empty"); 63 | } 64 | else{ 65 | cy.exec("cat cypress.json") 66 | .its("stderr") 67 | .should("be.empty"); 68 | 69 | cy.exec("pwd") 70 | .its("code") 71 | .should("eq", 0); 72 | } 73 | }); 74 | 75 | it("cy.focused() - get the DOM element that has focus", () => { 76 | // https://on.cypress.io/focused 77 | cy.get(".misc-form").find("#name") 78 | .click(); 79 | cy.focused().should("have.id", "name"); 80 | 81 | cy.get(".misc-form").find("#description") 82 | .click(); 83 | cy.focused().should("have.id", "description"); 84 | }); 85 | 86 | context("Cypress.Screenshot", function(){ 87 | it("cy.screenshot() - take a screenshot", () => { 88 | // https://on.cypress.io/screenshot 89 | cy.screenshot("my-image"); 90 | }); 91 | 92 | it("Cypress.Screenshot.defaults() - change default config of screenshots", function(){ 93 | Cypress.Screenshot.defaults({ 94 | blackout: [".foo"], 95 | capture: "viewport", 96 | clip: { 97 | x: 0, 98 | y: 0, 99 | width: 200, 100 | height: 200, 101 | }, 102 | scale: false, 103 | disableTimersAndAnimations: true, 104 | screenshotOnRunFailure: true, 105 | onBeforeScreenshot(){ }, 106 | onAfterScreenshot(){ }, 107 | }); 108 | }); 109 | }); 110 | 111 | it("cy.wrap() - wrap an object", () => { 112 | // https://on.cypress.io/wrap 113 | cy.wrap({ foo: "bar" }) 114 | .should("have.property", "foo") 115 | .and("include", "bar"); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/navigation.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Navigation", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io"); 6 | cy.get(".navbar-nav").contains("Commands") 7 | .click(); 8 | cy.get(".dropdown-menu").contains("Navigation") 9 | .click(); 10 | }); 11 | 12 | it("cy.go() - go back or forward in the browser's history", () => { 13 | // https://on.cypress.io/go 14 | 15 | cy.location("pathname").should("include", "navigation"); 16 | 17 | cy.go("back"); 18 | cy.location("pathname").should("not.include", "navigation"); 19 | 20 | cy.go("forward"); 21 | cy.location("pathname").should("include", "navigation"); 22 | 23 | // clicking back 24 | cy.go(-1); 25 | cy.location("pathname").should("not.include", "navigation"); 26 | 27 | // clicking forward 28 | cy.go(1); 29 | cy.location("pathname").should("include", "navigation"); 30 | }); 31 | 32 | it("cy.reload() - reload the page", () => { 33 | // https://on.cypress.io/reload 34 | cy.reload(); 35 | 36 | // reload the page without using the cache 37 | cy.reload(true); 38 | }); 39 | 40 | it("cy.visit() - visit a remote url", () => { 41 | // https://on.cypress.io/visit 42 | 43 | // Visit any sub-domain of your current domain 44 | 45 | // Pass options to the visit 46 | cy.visit("https://example.cypress.io/commands/navigation", { 47 | timeout: 50000, // increase total time for the visit to resolve 48 | onBeforeLoad(contentWindow){ 49 | // contentWindow is the remote page's window object 50 | expect(typeof contentWindow === "object").to.be.true; 51 | }, 52 | onLoad(contentWindow){ 53 | // contentWindow is the remote page's window object 54 | expect(typeof contentWindow === "object").to.be.true; 55 | }, 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/querying.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Querying", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/querying"); 6 | }); 7 | 8 | // The most commonly used query is 'cy.get()', you can 9 | // think of this like the '$' in jQuery 10 | 11 | it("cy.get() - query DOM elements", () => { 12 | // https://on.cypress.io/get 13 | 14 | cy.get("#query-btn").should("contain", "Button"); 15 | 16 | cy.get(".query-btn").should("contain", "Button"); 17 | 18 | cy.get("#querying .well>button:first").should("contain", "Button"); 19 | // ↲ 20 | // Use CSS selectors just like jQuery 21 | 22 | cy.get('[data-test-id="test-example"]').should("have.class", "example"); 23 | 24 | // 'cy.get()' yields jQuery object, you can get its attribute 25 | // by invoking `.attr()` method 26 | cy.get('[data-test-id="test-example"]') 27 | .invoke("attr", "data-test-id") 28 | .should("equal", "test-example"); 29 | 30 | // or you can get element's CSS property 31 | cy.get('[data-test-id="test-example"]') 32 | .invoke("css", "position") 33 | .should("equal", "static"); 34 | 35 | // or use assertions directly during 'cy.get()' 36 | // https://on.cypress.io/assertions 37 | cy.get('[data-test-id="test-example"]') 38 | .should( 39 | "have.attr", "data-test-id", "test-example" 40 | ) 41 | .and( 42 | "have.css", "position", "static" 43 | ); 44 | }); 45 | 46 | it("cy.contains() - query DOM elements with matching content", () => { 47 | // https://on.cypress.io/contains 48 | cy.get(".query-list") 49 | .contains("bananas") 50 | .should("have.class", "third"); 51 | 52 | // we can pass a regexp to `.contains()` 53 | cy.get(".query-list") 54 | .contains(/^b\w+/) 55 | .should("have.class", "third"); 56 | 57 | cy.get(".query-list") 58 | .contains("apples") 59 | .should("have.class", "first"); 60 | 61 | // passing a selector to contains will 62 | // yield the selector containing the text 63 | cy.get("#querying") 64 | .contains("ul", "oranges") 65 | .should("have.class", "query-list"); 66 | 67 | cy.get(".query-button") 68 | .contains("Save Form") 69 | .should("have.class", "btn"); 70 | }); 71 | 72 | it(".within() - query DOM elements within a specific element", () => { 73 | // https://on.cypress.io/within 74 | cy.get(".query-form").within(() => { 75 | cy.get("input:first").should( 76 | "have.attr", "placeholder", "Email" 77 | ); 78 | cy.get("input:last").should( 79 | "have.attr", "placeholder", "Password" 80 | ); 81 | }); 82 | }); 83 | 84 | it("cy.root() - query the root DOM element", () => { 85 | // https://on.cypress.io/root 86 | 87 | // By default, root is the document 88 | cy.root().should("match", "html"); 89 | 90 | cy.get(".query-ul").within(() => { 91 | // In this within, the root is now the ul DOM element 92 | cy.root().should("have.class", "query-ul"); 93 | }); 94 | }); 95 | 96 | it("best practices - selecting elements", () => { 97 | // https://on.cypress.io/best-practices#Selecting-Elements 98 | cy.get("[data-cy=best-practices-selecting-elements]").within(() => { 99 | // Worst - too generic, no context 100 | cy.get("button").click(); 101 | 102 | // Bad. Coupled to styling. Highly subject to change. 103 | cy.get(".btn.btn-large").click(); 104 | 105 | // Average. Coupled to the `name` attribute which has HTML semantics. 106 | cy.get("[name=submission]").click(); 107 | 108 | // Better. But still coupled to styling or JS event listeners. 109 | cy.get("#main").click(); 110 | 111 | // Slightly better. Uses an ID but also ensures the element 112 | // has an ARIA role attribute 113 | cy.get("#main[role=button]").click(); 114 | 115 | // Much better. But still coupled to text content that may change. 116 | cy.contains("Submit").click(); 117 | 118 | // Best. Insulated from all changes. 119 | cy.get("[data-cy=submit]").click(); 120 | }); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/traversal.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Traversal", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/traversal"); 6 | }); 7 | 8 | it(".children() - get child DOM elements", () => { 9 | // https://on.cypress.io/children 10 | cy.get(".traversal-breadcrumb") 11 | .children(".active") 12 | .should("contain", "Data"); 13 | }); 14 | 15 | it(".closest() - get closest ancestor DOM element", () => { 16 | // https://on.cypress.io/closest 17 | cy.get(".traversal-badge") 18 | .closest("ul") 19 | .should("have.class", "list-group"); 20 | }); 21 | 22 | it(".eq() - get a DOM element at a specific index", () => { 23 | // https://on.cypress.io/eq 24 | cy.get(".traversal-list>li") 25 | .eq(1) 26 | .should("contain", "siamese"); 27 | }); 28 | 29 | it(".filter() - get DOM elements that match the selector", () => { 30 | // https://on.cypress.io/filter 31 | cy.get(".traversal-nav>li") 32 | .filter(".active") 33 | .should("contain", "About"); 34 | }); 35 | 36 | it(".find() - get descendant DOM elements of the selector", () => { 37 | // https://on.cypress.io/find 38 | cy.get(".traversal-pagination") 39 | .find("li") 40 | .find("a") 41 | .should("have.length", 7); 42 | }); 43 | 44 | it(".first() - get first DOM element", () => { 45 | // https://on.cypress.io/first 46 | cy.get(".traversal-table td") 47 | .first() 48 | .should("contain", "1"); 49 | }); 50 | 51 | it(".last() - get last DOM element", () => { 52 | // https://on.cypress.io/last 53 | cy.get(".traversal-buttons .btn") 54 | .last() 55 | .should("contain", "Submit"); 56 | }); 57 | 58 | it(".next() - get next sibling DOM element", () => { 59 | // https://on.cypress.io/next 60 | cy.get(".traversal-ul") 61 | .contains("apples") 62 | .next() 63 | .should("contain", "oranges"); 64 | }); 65 | 66 | it(".nextAll() - get all next sibling DOM elements", () => { 67 | // https://on.cypress.io/nextall 68 | cy.get(".traversal-next-all") 69 | .contains("oranges") 70 | .nextAll() 71 | .should("have.length", 3); 72 | }); 73 | 74 | it(".nextUntil() - get next sibling DOM elements until next el", () => { 75 | // https://on.cypress.io/nextuntil 76 | cy.get("#veggies") 77 | .nextUntil("#nuts") 78 | .should("have.length", 3); 79 | }); 80 | 81 | it(".not() - remove DOM elements from set of DOM elements", () => { 82 | // https://on.cypress.io/not 83 | cy.get(".traversal-disabled .btn") 84 | .not("[disabled]") 85 | .should("not.contain", "Disabled"); 86 | }); 87 | 88 | it(".parent() - get parent DOM element from DOM elements", () => { 89 | // https://on.cypress.io/parent 90 | cy.get(".traversal-mark") 91 | .parent() 92 | .should("contain", "Morbi leo risus"); 93 | }); 94 | 95 | it(".parents() - get parent DOM elements from DOM elements", () => { 96 | // https://on.cypress.io/parents 97 | cy.get(".traversal-cite") 98 | .parents() 99 | .should("match", "blockquote"); 100 | }); 101 | 102 | it(".parentsUntil() - get parent DOM elements from DOM elements until el", () => { 103 | // https://on.cypress.io/parentsuntil 104 | cy.get(".clothes-nav") 105 | .find(".active") 106 | .parentsUntil(".clothes-nav") 107 | .should("have.length", 2); 108 | }); 109 | 110 | it(".prev() - get previous sibling DOM element", () => { 111 | // https://on.cypress.io/prev 112 | cy.get(".birds").find(".active") 113 | .prev() 114 | .should("contain", "Lorikeets"); 115 | }); 116 | 117 | it(".prevAll() - get all previous sibling DOM elements", () => { 118 | // https://on.cypress.io/prevAll 119 | cy.get(".fruits-list").find(".third") 120 | .prevAll() 121 | .should("have.length", 2); 122 | }); 123 | 124 | it(".prevUntil() - get all previous sibling DOM elements until el", () => { 125 | // https://on.cypress.io/prevUntil 126 | cy.get(".foods-list").find("#nuts") 127 | .prevUntil("#veggies") 128 | .should("have.length", 3); 129 | }); 130 | 131 | it(".siblings() - get all sibling DOM elements", () => { 132 | // https://on.cypress.io/siblings 133 | cy.get(".traversal-pills .active") 134 | .siblings() 135 | .should("have.length", 2); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/utilities.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Utilities", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/utilities"); 6 | }); 7 | 8 | it("Cypress._ - call a lodash method", () => { 9 | // https://on.cypress.io/_ 10 | cy.request("https://jsonplaceholder.cypress.io/users") 11 | .then(response => { 12 | const ids = Cypress._.chain(response.body).map("id") 13 | .take(3) 14 | .value(); 15 | 16 | expect(ids).to.deep.eq([ 17 | 1, 2, 3, 18 | ]); 19 | }); 20 | }); 21 | 22 | it("Cypress.$ - call a jQuery method", () => { 23 | // https://on.cypress.io/$ 24 | const $li = Cypress.$(".utility-jquery li:first"); 25 | 26 | cy.wrap($li) 27 | .should("not.have.class", "active") 28 | .click() 29 | .should("have.class", "active"); 30 | }); 31 | 32 | it("Cypress.Blob - blob utilities and base64 string conversion", () => { 33 | // https://on.cypress.io/blob 34 | cy.get(".utility-blob").then($div => { 35 | // https://github.com/nolanlawson/blob-util#imgSrcToDataURL 36 | // get the dataUrl string for the javascript-logo 37 | return Cypress.Blob.imgSrcToDataURL( 38 | "https://example.cypress.io/assets/img/javascript-logo.png", undefined, "anonymous" 39 | ) 40 | .then(dataUrl => { 41 | // create an element and set its src to the dataUrl 42 | const img = Cypress.$("", { src: dataUrl }); 43 | 44 | // need to explicitly return cy here since we are initially returning 45 | // the Cypress.Blob.imgSrcToDataURL promise to our test 46 | // append the image 47 | $div.append(img); 48 | 49 | cy.get(".utility-blob img").click() 50 | .should( 51 | "have.attr", "src", dataUrl 52 | ); 53 | }); 54 | }); 55 | }); 56 | 57 | it("Cypress.minimatch - test out glob patterns against strings", () => { 58 | // https://on.cypress.io/minimatch 59 | let matching = Cypress.minimatch( 60 | "/users/1/comments", "/users/*/comments", { matchBase: true } 61 | ); 62 | 63 | expect(matching, "matching wildcard").to.be.true; 64 | 65 | matching = Cypress.minimatch( 66 | "/users/1/comments/2", "/users/*/comments", { matchBase: true } 67 | ); 68 | 69 | expect(matching, "comments").to.be.false; 70 | 71 | // ** matches against all downstream path segments 72 | matching = Cypress.minimatch( 73 | "/foo/bar/baz/123/quux?a=b&c=2", "/foo/**", { matchBase: true } 74 | ); 75 | 76 | expect(matching, "comments").to.be.true; 77 | 78 | // whereas * matches only the next path segment 79 | 80 | matching = Cypress.minimatch( 81 | "/foo/bar/baz/123/quux?a=b&c=2", "/foo/*", { matchBase: false } 82 | ); 83 | 84 | expect(matching, "comments").to.be.false; 85 | }); 86 | 87 | it("Cypress.moment() - format or parse dates using a moment method", () => { 88 | // https://on.cypress.io/moment 89 | const time = Cypress.moment("2014-04-25T19:38:53.196Z").utc() 90 | .format("h:mm A"); 91 | 92 | expect(time).to.be.a("string"); 93 | 94 | cy.get(".utility-moment").contains("3:38 PM") 95 | .should("have.class", "badge"); 96 | 97 | // the time in the element should be between 3pm and 5pm 98 | const start = Cypress.moment("3:00 PM", "LT"); 99 | const end = Cypress.moment("5:00 PM", "LT"); 100 | 101 | cy.get(".utility-moment .badge") 102 | .should($el => { 103 | // parse American time like "3:38 PM" 104 | const m = Cypress.moment($el.text().trim(), "LT"); 105 | 106 | // display hours + minutes + AM|PM 107 | const f = "h:mm A"; 108 | 109 | expect(m.isBetween(start, end), 110 | `${m.format(f)} should be between ${start.format(f)} and ${end.format(f)}`).to.be.true; 111 | }); 112 | }); 113 | 114 | it("Cypress.Promise - instantiate a bluebird promise", () => { 115 | // https://on.cypress.io/promise 116 | let waited = false; 117 | 118 | /** 119 | * @return Bluebird 120 | */ 121 | function waitOneSecond(){ 122 | // return a promise that resolves after 1 second 123 | // @ts-ignore TS2351 (new Cypress.Promise) 124 | return new Cypress.Promise((resolve, reject) => { 125 | setTimeout(() => { 126 | // set waited to true 127 | waited = true; 128 | 129 | // resolve with 'foo' string 130 | resolve("foo"); 131 | }, 1000); 132 | }); 133 | } 134 | 135 | cy.then(() => { 136 | // return a promise to cy.then() that 137 | // is awaited until it resolves 138 | // @ts-ignore TS7006 139 | return waitOneSecond().then(str => { 140 | expect(str).to.eq("foo"); 141 | expect(waited).to.be.true; 142 | }); 143 | }); 144 | }); 145 | }); 146 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/viewport.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Viewport", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/viewport"); 6 | }); 7 | 8 | it("cy.viewport() - set the viewport size and dimension", () => { 9 | // https://on.cypress.io/viewport 10 | 11 | cy.get("#navbar").should("be.visible"); 12 | cy.viewport(320, 480); 13 | 14 | // the navbar should have collapse since our screen is smaller 15 | cy.get("#navbar").should("not.be.visible"); 16 | cy.get(".navbar-toggle").should("be.visible") 17 | .click(); 18 | cy.get(".nav").find("a") 19 | .should("be.visible"); 20 | 21 | // lets see what our app looks like on a super large screen 22 | cy.viewport(2999, 2999); 23 | 24 | // cy.viewport() accepts a set of preset sizes 25 | // to easily set the screen to a device's width and height 26 | 27 | // We added a cy.wait() between each viewport change so you can see 28 | // the change otherwise it is a little too fast to see :) 29 | 30 | cy.viewport("macbook-15"); 31 | cy.wait(200); 32 | cy.viewport("macbook-13"); 33 | cy.wait(200); 34 | cy.viewport("macbook-11"); 35 | cy.wait(200); 36 | cy.viewport("ipad-2"); 37 | cy.wait(200); 38 | cy.viewport("ipad-mini"); 39 | cy.wait(200); 40 | cy.viewport("iphone-6+"); 41 | cy.wait(200); 42 | cy.viewport("iphone-6"); 43 | cy.wait(200); 44 | cy.viewport("iphone-5"); 45 | cy.wait(200); 46 | cy.viewport("iphone-4"); 47 | cy.wait(200); 48 | cy.viewport("iphone-3"); 49 | cy.wait(200); 50 | 51 | // cy.viewport() accepts an orientation for all presets 52 | // the default orientation is 'portrait' 53 | cy.viewport("ipad-2", "portrait"); 54 | cy.wait(200); 55 | cy.viewport("iphone-4", "landscape"); 56 | cy.wait(200); 57 | 58 | // The viewport will be reset back to the default dimensions 59 | // in between tests (the default can be set in cypress.json) 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/waiting.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Waiting", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/waiting"); 6 | }); 7 | // BE CAREFUL of adding unnecessary wait times. 8 | // https://on.cypress.io/best-practices#Unnecessary-Waiting 9 | 10 | // https://on.cypress.io/wait 11 | it("cy.wait() - wait for a specific amount of time", () => { 12 | cy.get(".wait-input1").type("Wait 1000ms after typing"); 13 | cy.wait(1000); 14 | cy.get(".wait-input2").type("Wait 1000ms after typing"); 15 | cy.wait(1000); 16 | cy.get(".wait-input3").type("Wait 1000ms after typing"); 17 | cy.wait(1000); 18 | }); 19 | 20 | it("cy.wait() - wait for a specific route", () => { 21 | cy.server(); 22 | 23 | // Listen to GET to comments/1 24 | cy.route("GET", "comments/*").as("getComment"); 25 | 26 | // we have code that gets a comment when 27 | // the button is clicked in scripts.js 28 | cy.get(".network-btn").click(); 29 | 30 | // wait for GET comments/1 31 | cy.wait("@getComment").its("status") 32 | .should("eq", 200); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /dev/js/e2e/tests/examples/window.e2e.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context("Window", () => { 4 | beforeEach(() => { 5 | cy.visit("https://example.cypress.io/commands/window"); 6 | }); 7 | 8 | it("cy.window() - get the global window object", () => { 9 | // https://on.cypress.io/window 10 | cy.window().should("have.property", "top"); 11 | }); 12 | 13 | it("cy.document() - get the document object", () => { 14 | // https://on.cypress.io/document 15 | cy.document().should("have.property", "charset") 16 | .and("eq", "UTF-8"); 17 | }); 18 | 19 | it("cy.title() - get the title", () => { 20 | // https://on.cypress.io/title 21 | cy.title().should("include", "Kitchen Sink"); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /dev/js/mainDemo.js: -------------------------------------------------------------------------------- 1 | import { newVueInstance } from "@/vue"; 2 | import router from "@/vue/router"; 3 | import store from "@/vue/store"; 4 | import Demo from "@components/Demo"; 5 | import Hamburger from "@components/Hamburger"; 6 | import { browserDetectorOf } from "@js/utils/BrowserDetector"; 7 | 8 | (() => { 9 | console.log( 10 | "%c VOLTRA ", 11 | `background: #0087b3; 12 | color: white; 13 | font-size: 17px; 14 | font-weight: bold; 15 | line-height: 36px; 16 | text-align: center; 17 | border-radius: 50vw;`, 18 | "www.ludwigguerin.fr" 19 | ); 20 | 21 | const browserDetector = browserDetectorOf(window).bootstrapClasses(); 22 | 23 | const vm = newVueInstance({ 24 | el: "#app", 25 | router, 26 | store, 27 | components: { 28 | Demo, 29 | Hamburger, 30 | }, 31 | data(){ 32 | return { menuOpened: false }; 33 | }, 34 | }); 35 | })(); 36 | -------------------------------------------------------------------------------- /dev/js/tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | env: { 5 | node: true, 6 | browser: false, 7 | }, 8 | rules: { "no-undef": "off" }, 9 | }; 10 | -------------------------------------------------------------------------------- /dev/js/tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/js/tests/.gitkeep -------------------------------------------------------------------------------- /dev/js/tests/demo.test.js: -------------------------------------------------------------------------------- 1 | import { add } from "../demo"; 2 | 3 | describe("Demo", function(){ 4 | it("should do math", function(){ 5 | expect(1 + 1).toBe(2); 6 | }); 7 | 8 | it("should get additions right", function(){ 9 | expect(add(40, 2)).toBe(42); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /dev/js/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {"development"|"production"|"test"} NodeEnv 3 | * @typedef {string|number|boolean|undefined} EnvVariable 4 | */ 5 | 6 | 7 | /** 8 | * Utility for environment variable checking 9 | * @type {{__: {cached: Record}, readonly prod: boolean, readonly dev: boolean, readonly test: boolean, get(string, EnvVariable=): EnvVariable, is(NodeEnv): boolean}} 10 | */ 11 | export const Env = { 12 | /** 13 | * @private 14 | */ 15 | __: { 16 | /** 17 | * @type {Record} 18 | */ 19 | cached: {}, 20 | }, 21 | 22 | /** 23 | * Retrieve an environment variable 24 | * @param {string} name - The environment variable's name 25 | * @param {EnvVariable} defaultValue - The value to provide if it does not exist 26 | * @returns {EnvVariable} 27 | */ 28 | get(name, defaultValue = undefined){ 29 | if(name in this.__.cached) 30 | return this.__.cached[name]; 31 | 32 | // eslint-disable-next-line no-undef 33 | const val = process.env?.[name] ?? defaultValue; 34 | 35 | if(typeof val === "string"){ 36 | if(["true", "false"].includes(val)) 37 | return val === "true"; 38 | 39 | const asInt = parseInt(val, 10); 40 | if(isNaN(asInt)){ 41 | this.__.cached[name] = val; 42 | return val; 43 | } 44 | 45 | const asFloat = parseFloat(val); 46 | const ret = asInt === asFloat ? asInt : asFloat; 47 | this.__.cached[name] = ret; 48 | return ret; 49 | } 50 | 51 | this.__.cached[name] = val; 52 | return val; 53 | }, 54 | 55 | /** 56 | * Determine whether or not the current mode is the provided one 57 | * @param {NodeEnv} mode 58 | * @returns {boolean} 59 | */ 60 | is(mode){ 61 | return this.get("NODE_ENV") === mode; 62 | }, 63 | 64 | /** 65 | * Determine whether or not the current mode is development 66 | * @returns {boolean} 67 | */ 68 | get dev(){ 69 | return this.is("development"); 70 | }, 71 | 72 | /** 73 | * Determine whether or not the current mode is production 74 | * @returns {boolean} 75 | */ 76 | get prod(){ 77 | return this.is("production"); 78 | }, 79 | 80 | /** 81 | * Determine whether or not the current mode is test 82 | * @returns {boolean} 83 | */ 84 | get test(){ 85 | return this.is("test"); 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /dev/js/utils/BrowserDetector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class BrowserDetector 3 | * @classdesc A class used for JS browser/platform detection 4 | */ 5 | class BrowserDetector{ 6 | /** 7 | * Construct a new {@link BrowserDetector} 8 | * @param {typeof globalThis} global 9 | */ 10 | constructor(global = window){ 11 | this.global = global; 12 | this.navigator = this.global.navigator; 13 | this.ua = this.navigator.userAgent.toLowerCase(); 14 | this.vendor = this.navigator.vendor.toLowerCase() || ""; 15 | this.platform = this.navigator.platform.toLowerCase(); 16 | 17 | this.bootstrapped = false; 18 | } 19 | 20 | // CSS Setup 21 | /** 22 | * Add CSS classes to the document (on the tag) 23 | * @returns {BrowserDetector} 24 | */ 25 | bootstrapClasses(){ 26 | if(this.bootstrapped) 27 | return this; 28 | 29 | const html = this.global.document.documentElement; 30 | 31 | [ 32 | //Browsers 33 | "ie", 34 | "edge", 35 | "chrome", 36 | "opera", 37 | "firefox", 38 | "safari", 39 | "vivaldi", 40 | 41 | //Specific Browsers 42 | "chromeIOS", 43 | "ieMobile", 44 | 45 | //Platforms 46 | "windows", 47 | "mac", 48 | "linux", 49 | "android", 50 | "blackberry", 51 | "ios", 52 | 53 | //Type 54 | "desktop", 55 | "mobile", 56 | ].forEach(browser => { 57 | if(this[browser]) 58 | html.classList.add(browser); 59 | }); 60 | 61 | return this; 62 | } 63 | 64 | // Pure browsers 65 | get ie(){ 66 | return this.ua.includes("msie") || this.ua.includes("trident"); 67 | } 68 | 69 | get edge(){ 70 | return this.ua.includes("edg"); // new edge uses Edg instead of Edge 71 | } 72 | 73 | get chrome(){ 74 | return this.ua.includes("chrome") 75 | && this.vendor.includes("google") 76 | && !this.opera 77 | //&& !this.safari() 78 | && !this.vivaldi; 79 | } 80 | 81 | get opera(){ 82 | return typeof this.global.opr !== "undefined"; 83 | } 84 | 85 | get firefox(){ 86 | return this.ua.includes("firefox"); 87 | } 88 | 89 | get safari(){ 90 | return this.ua.includes("safari") 91 | && !this.vivaldi 92 | && !this.chrome 93 | && !this.edge; 94 | } 95 | 96 | get vivaldi(){ 97 | return this.ua.includes("vivaldi"); 98 | } 99 | 100 | 101 | // Specific browsers 102 | get chromeIOS(){ 103 | return this.ua.includes("crios"); 104 | } 105 | 106 | get ieMobile(){ 107 | return this.ua.includes("iemobile"); 108 | } 109 | 110 | // Platform 111 | get windows(){ 112 | return this.platform.includes("win"); 113 | } 114 | 115 | get mac(){ 116 | return this.platform.includes("mac"); 117 | } 118 | 119 | get linux(){ 120 | return this.platform.includes("linux"); 121 | } 122 | 123 | get android(){ 124 | return this.ua.includes("android"); 125 | } 126 | 127 | get ios(){ 128 | return /i(phone|pad|pod)/i.test(this.ua); 129 | } 130 | 131 | get blackberry(){ 132 | return this.ua.includes("blackberry"); 133 | } 134 | 135 | // Type 136 | get desktop(){ 137 | return !this.mobile; 138 | } 139 | 140 | get mobile(){ 141 | return [ 142 | "chromeIOS", 143 | "ieMobile", 144 | 145 | "android", 146 | "ios", 147 | "blackberry", 148 | ].some(browser => this[browser]); 149 | } 150 | } 151 | 152 | const browserDetectorOf = (global = window) => new BrowserDetector(global); 153 | 154 | export { browserDetectorOf }; 155 | -------------------------------------------------------------------------------- /dev/middlewares.php: -------------------------------------------------------------------------------- 1 | getContainer(); 19 | $response = new Response(); 20 | //TODO: Custom error handling 21 | //TODO: Custom rendering for deifferent status 22 | 23 | $status = 404; 24 | return $container->view->render($response->withStatus($status), "errors/$status.twig", []); 25 | }; 26 | } 27 | 28 | return static function(\Slim\App $app, \DI\Container $container, $config, $settings){ 29 | $displayErrorDetails = $settings["displayErrorDetails"]; 30 | $logErrors = $settings["logErrors"]; 31 | $logErrorDetails = $settings["logErrorDetails"]; 32 | $logger = $container->get("logger"); 33 | 34 | $app->add(\App\Middlewares\RouteModelBinding::from($container)); //WARNING: Experimental due to Slim4's new way of handling request parameters... 35 | $app->addRoutingMiddleware(); 36 | $app->addBodyParsingMiddleware(); 37 | 38 | $app->add(new TrailingSlash(false)) // Force remove trailing slash 39 | ->add(new MethodOverrideMiddleware()) // Allow method override in forms 40 | ->add(new ContentLengthMiddleware()) // Add correct content length 41 | ->add(new Session($config["session"])) 42 | ->add(TwigMiddleware::createFromContainer($app)) 43 | ->add(\App\Middlewares\RedirectAfterRequest::from($container)) 44 | ->add(\App\Middlewares\Csrf::from($container)) 45 | ->add(\App\Middlewares\Auth::from($container)) 46 | ->add(\App\Middlewares\RequestBinding::from($container)) // Add request to the container 47 | ->add(new WhoopsMiddleware()); 48 | 49 | $eh = new ExceptionHandler($app->getCallableResolver(), $app->getResponseFactory()); 50 | $request = ServerRequestCreatorFactory::create()->createServerRequestFromGlobals(); 51 | $lh = new LegacyPhpErrorHandler($request, $eh, $displayErrorDetails, $logErrors, $logErrorDetails); 52 | register_shutdown_function($lh); 53 | 54 | $app->addErrorMiddleware( 55 | $displayErrorDetails, 56 | $logErrors, 57 | $logErrorDetails, 58 | $logger 59 | )->setDefaultErrorHandler($eh); 60 | }; 61 | -------------------------------------------------------------------------------- /dev/resources/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/resources/favicon.png -------------------------------------------------------------------------------- /dev/resources/img/jpg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/resources/img/jpg/.gitkeep -------------------------------------------------------------------------------- /dev/resources/img/png/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/resources/img/png/.gitkeep -------------------------------------------------------------------------------- /dev/resources/img/svg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/resources/img/svg/.gitkeep -------------------------------------------------------------------------------- /dev/routes/2fa.php: -------------------------------------------------------------------------------- 1 | get("/2fa/{__user}", cm(TwoFactorController::class, "twoFactor")) 15 | ->add(filter(UserFilter::class)) 16 | ->setName("demo.2fa"); 17 | 18 | $app->get("/2fa/enable/{__user}", cm(TwoFactorController::class, "enable2FA")) 19 | ->add(filter(UserFilter::class)) 20 | ->setName("demo.2fa.enable"); 21 | 22 | $app->get("/2fa/disable/{__user}", cm(TwoFactorController::class, "disable2FA")) 23 | ->add(filter(UserFilter::class)) 24 | ->setName("demo.2fa.disable"); 25 | -------------------------------------------------------------------------------- /dev/routes/auth.php: -------------------------------------------------------------------------------- 1 | getContainer(), [ 22 | "username" => "username", 23 | "code" => "2fa", 24 | ]);*/ 25 | $requires2FA = requires2FA([ 26 | "username" => "username", 27 | "code" => "2fa", 28 | ]); 29 | 30 | $app->get("/auth/login", cm( 31 | AuthController::class, 32 | "loginForm" 33 | ))->setName("auth.login") 34 | ->add($forVisitor); 35 | 36 | $app->get("/auth/register", cm( 37 | AuthController::class, 38 | "registerForm" 39 | ))->setName("auth.register") 40 | ->add($forVisitor); 41 | 42 | $app->post("/auth/login", cm( 43 | AuthController::class, 44 | "login" 45 | ))->setName("auth.login.post") 46 | ->add($requires2FA) 47 | ->add($forVisitor); 48 | 49 | $app->post("/auth/register", cm( 50 | AuthController::class, 51 | "register" 52 | ))->setName("auth.register.post") 53 | ->add($forVisitor); 54 | 55 | $app->get("/auth/logout", cm( 56 | AuthController::class, 57 | "logout" 58 | ))->setName("auth.logout") 59 | ->add($forLogout); 60 | 61 | $app->get("/auth/force-login/{__user}", function(ServerRequestInterface $req, Response $res, User $user){ 62 | /** 63 | * @var Auth $auth 64 | */ 65 | $auth = resolve(Auth::class); 66 | 67 | /** 68 | * @var \App\Actions\Response $responseUtils 69 | */ 70 | $responseUtils = resolve(\App\Actions\Response::class); 71 | 72 | $response = $auth->forceLogin($res, $user->username)->response; 73 | return $responseUtils->redirectToRoute($response, "home"); 74 | })->setName("auth.force_login") 75 | ->add($forVisitor); 76 | -------------------------------------------------------------------------------- /dev/routes/demo.php: -------------------------------------------------------------------------------- 1 | get("/", cm(DemoController::class, "home")) 15 | ->setName("home"); 16 | -------------------------------------------------------------------------------- /dev/routes/demo2.php: -------------------------------------------------------------------------------- 1 | get("/user/{user}", cm(\App\Controllers\DemoController::class, "user")) 12 | ->setName("demo2"); 13 | -------------------------------------------------------------------------------- /dev/routes/demo3.php: -------------------------------------------------------------------------------- 1 | get("/rmb/{__user}", Routing::cm(\App\Controllers\DemoController::class, "vmb")) 15 | ->setName("demo3"); 16 | -------------------------------------------------------------------------------- /dev/routes/redir.php: -------------------------------------------------------------------------------- 1 | get("/redirectable", function(ServerRequestInterface $req, \Slim\Psr7\Response $res){ 11 | return $res; 12 | })->add(\App\Middlewares\AcceptsRedirect::from($app->getContainer())) 13 | ->setName("redirectable"); 14 | -------------------------------------------------------------------------------- /dev/scss/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/scss/.gitkeep -------------------------------------------------------------------------------- /dev/scss/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/scss/components/.gitkeep -------------------------------------------------------------------------------- /dev/scss/components/Hamburger.scss: -------------------------------------------------------------------------------- 1 | @import "~@scss/libraries/hamburger"; 2 | 3 | .hamburger{ 4 | position: fixed; 5 | top: 0; 6 | right: 0; 7 | z-index: 20; 8 | 9 | display: block; 10 | outline: none; 11 | 12 | opacity: 0.9; 13 | &:hover{ 14 | opacity: 1; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dev/scss/libraries/_hamburger.scss: -------------------------------------------------------------------------------- 1 | $hamburger-layer-color: black; 2 | $hamburger-types: (emphatic); 3 | 4 | $hamburger-layer-width: 40px; 5 | $hamburger-layer-height: 4px; 6 | $hamburger-layer-spacing: 1.5 * $hamburger-layer-height; 7 | $hamburger-layer-border-radius: $hamburger-layer-height; 8 | 9 | @import "~hamburgers/_sass/hamburgers/hamburgers"; 10 | 11 | $height: (2*$hamburger-padding-y) + (3*$hamburger-layer-height) + (2*$hamburger-layer-spacing); 12 | $width: (2*$hamburger-padding-x) + $hamburger-layer-width; 13 | $padding: $hamburger-padding-y; 14 | -------------------------------------------------------------------------------- /dev/scss/libraries/_mq.scss: -------------------------------------------------------------------------------- 1 | @use "~sass-mq" as sassMq with ( 2 | $mq-breakpoints: ( 3 | xsmall: 540, 4 | small: 900, 5 | medium: 1280, 6 | large: 1440, 7 | xlarge: 1920 8 | // xxlarge: Infinity 9 | ) 10 | ); 11 | 12 | /* 13 | { 14 | xsmall: 540, // up to 540 => xsmall 15 | small: 900, // from 540 up to 900 => small 16 | medium: 1280, 17 | large: 1440, 18 | xlarge: 1920, 19 | xxlarge: Infinity, 20 | } 21 | */ 22 | 23 | /// Add CSS from a specific breakpoint 24 | /// @param {keyword} $keyword - The desired breakpoint as a keyword 25 | /// @content The CSS to be applied when matching the specified breakpoint 26 | /// @see mq 27 | @mixin breakpoint($keyword){ 28 | @include sassMq.mq($from: $keyword){ 29 | @content; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dev/scss/libraries/index.scss: -------------------------------------------------------------------------------- 1 | @import "mq"; 2 | @import "hamburger"; 3 | -------------------------------------------------------------------------------- /dev/scss/mixins/_browser.scss: -------------------------------------------------------------------------------- 1 | /// Block mixin that adds styles for the given browser 2 | /// @param {string|keyword} $browsers - The browser(s) combination(s) to style for 3 | /// @content Styles that will be applied only if the browser matches the provided one 4 | /// @example scss - Browser mixin 5 | /// @include browser(ie){} 6 | /// // styles only IE 7 | /// 8 | /// @include browser("desktop.windows"){} 9 | /// // styles only on desktop versions of windows 10 | @mixin browser($browsers...) { 11 | $selector: &; 12 | 13 | @each $b in $browsers { 14 | $browser: unquote(quote($b)); // Trick to be able to pass class combos 15 | /* 16 | Allows: 17 | browser(ie) 18 | browser("ie.mobile") 19 | browser("firefox.android") 20 | browser(ie, "firefox.ios") 21 | etc... 22 | */ 23 | 24 | @at-root { 25 | html.#{$browser} { 26 | #{$selector} { 27 | @content; 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /dev/scss/mixins/_margin.scss: -------------------------------------------------------------------------------- 1 | $defaultMargin: 0 !default; 2 | 3 | @mixin verticalMargin($value: $defaultMargin) { 4 | margin-top: $value; 5 | margin-bottom: $value; 6 | } 7 | 8 | @mixin horizontalMargin($value: $defaultMargin) { 9 | margin-left: $value; 10 | margin-right: $value; 11 | } 12 | 13 | @mixin centerMargin { 14 | @include horizontalMargin(auto); 15 | } 16 | 17 | //// 18 | 19 | @mixin marginTR($value: $defaultMargin) { 20 | margin-top: $value; 21 | margin-right: $value; 22 | } 23 | 24 | @mixin marginTL($value: $defaultMargin) { 25 | margin-top: $value; 26 | margin-left: $value; 27 | } 28 | 29 | @mixin marginBR($value: $defaultMargin) { 30 | margin-bottom: $value; 31 | margin-right: $value; 32 | } 33 | 34 | @mixin marginBL($value: $defaultMargin) { 35 | margin-bottom: $value; 36 | margin-left: $value; 37 | } 38 | 39 | //// 40 | 41 | @mixin marginTRL($value: $defaultMargin) { 42 | @include horizontalMargin($value); 43 | margin-top: $value; 44 | } 45 | 46 | @mixin marginRBL($value: $defaultMargin) { 47 | @include horizontalMargin($value); 48 | margin-bottom: $value; 49 | } 50 | 51 | @mixin marginTBL($value: $defaultMargin) { 52 | @include verticalMargin($value); 53 | margin-left: $value; 54 | } 55 | 56 | @mixin marginTRB($value: $defaultMargin) { 57 | @include verticalMargin($value); 58 | margin-right: $value; 59 | } 60 | -------------------------------------------------------------------------------- /dev/scss/mixins/_padding.scss: -------------------------------------------------------------------------------- 1 | $defaultPadding: 0 !default; 2 | 3 | @mixin verticalPadding($value: $defaultPadding) { 4 | padding-top: $value; 5 | padding-bottom: $value; 6 | } 7 | 8 | @mixin horizontalPadding($value: $defaultPadding) { 9 | padding-left: $value; 10 | padding-right: $value; 11 | } 12 | 13 | //// 14 | 15 | @mixin paddingTR($value: $defaultPadding) { 16 | padding-top: $value; 17 | padding-right: $value; 18 | } 19 | 20 | @mixin paddingTL($value: $defaultPadding) { 21 | padding-top: $value; 22 | padding-left: $value; 23 | } 24 | 25 | @mixin paddingBR($value: $defaultPadding) { 26 | padding-bottom: $value; 27 | padding-right: $value; 28 | } 29 | 30 | @mixin paddingBL($value: $defaultPadding) { 31 | padding-bottom: $value; 32 | padding-left: $value; 33 | } 34 | 35 | //// 36 | 37 | @mixin paddingTRL($value: $defaultPadding) { 38 | @include horizontalPadding($value); 39 | padding-top: $value; 40 | } 41 | 42 | @mixin paddingRBL($value: $defaultPadding) { 43 | @include horizontalPadding($value); 44 | padding-bottom: $value; 45 | } 46 | 47 | @mixin paddingTBL($value: $defaultPadding) { 48 | @include verticalPadding($value); 49 | padding-left: $value; 50 | } 51 | 52 | @mixin paddingTRB($value: $defaultPadding) { 53 | @include verticalPadding($value); 54 | padding-right: $value; 55 | } 56 | -------------------------------------------------------------------------------- /dev/scss/mixins/_radius.scss: -------------------------------------------------------------------------------- 1 | $defaultRadius: 50vw; 2 | 3 | // ╔ ╗ ╚ ╝ ┌ ┐ └ ┘ 4 | 5 | @mixin radius($value: $defaultRadius) { 6 | /* 7 | ╔ ╗ 8 | 9 | ╚ ╝ 10 | */ 11 | border-radius: $defaultRadius; 12 | } 13 | 14 | @mixin noRadius { 15 | /* 16 | ┌ ┐ 17 | 18 | └ ┘ 19 | */ 20 | @include radius(0); 21 | } 22 | 23 | //// 24 | 25 | @mixin radiusTR($value: $defaultRadius) { 26 | /* 27 | ┌ ╗ 28 | 29 | └ ┘ 30 | */ 31 | border-top-right-radius: $value; 32 | } 33 | 34 | @mixin radiusTL($value: $defaultRadius) { 35 | /* 36 | ╔ ┐ 37 | 38 | └ ┘ 39 | */ 40 | border-top-left-radius: $value; 41 | } 42 | 43 | @mixin radiusBR($value: $defaultRadius) { 44 | /* 45 | ┌ ┐ 46 | 47 | └ ╝ 48 | */ 49 | border-bottom-right-radius: $value; 50 | } 51 | 52 | @mixin radiusBL($value: $defaultRadius) { 53 | /* 54 | ┌ ┐ 55 | 56 | ╚ ┘ 57 | */ 58 | border-bottom-left-radius: $value; 59 | } 60 | 61 | //// 62 | 63 | @mixin radiusTop($value: $defaultRadius) { 64 | /* 65 | ╔ ╗ 66 | 67 | └ ┘ 68 | */ 69 | @include radiusTL($value); 70 | @include radiusTR($value); 71 | } 72 | 73 | @mixin radiusRight($value: $defaultRadius) { 74 | /* 75 | ┌ ╗ 76 | 77 | └ ╝ 78 | */ 79 | @include radiusTR($value); 80 | @include radiusBR($value); 81 | } 82 | 83 | @mixin radiusBottom($value: $defaultRadius) { 84 | /* 85 | ┌ ┐ 86 | 87 | ╚ ╝ 88 | */ 89 | @include radiusBL($value); 90 | @include radiusBR($value); 91 | } 92 | 93 | @mixin radiusLeft($value: $defaultRadius) { 94 | /* 95 | ╔ ┐ 96 | 97 | ╚ ┘ 98 | */ 99 | @include radiusTL($value); 100 | @include radiusBL($value); 101 | } 102 | 103 | //// 104 | 105 | @mixin radiusDiagTL($value: $defaultRadius) { 106 | /* 107 | ╔ ┐ 108 | 109 | └ ╝ 110 | */ 111 | @include radiusTL($value); 112 | @include radiusBR($value); 113 | } 114 | 115 | @mixin radiusDiagBR($value: $defaultRadius) { 116 | /* 117 | ╔ ┐ 118 | 119 | └ ╝ 120 | */ 121 | @include radiusDiagTL($value); 122 | } 123 | 124 | @mixin radiusDiagTR($value: $defaultRadius) { 125 | /* 126 | ┌ ╗ 127 | 128 | ╚ ┘ 129 | */ 130 | @include radiusTR($value); 131 | @include radiusBL($value); 132 | } 133 | 134 | @mixin radiusDiagBL($value: $defaultRadius) { 135 | /* 136 | ┌ ╗ 137 | 138 | ╚ ┘ 139 | */ 140 | @include radiusDiagTR($value); 141 | } 142 | 143 | /// 144 | 145 | @mixin radiusAllButTR($value: $defaultRadius) { 146 | /* 147 | ╔ ┐ 148 | 149 | ╚ ╝ 150 | */ 151 | @include radiusLeft($value); 152 | @include radiusBR($value); 153 | } 154 | 155 | @mixin radiusAllButTL($value: $defaultRadius) { 156 | /* 157 | ┌ ╗ 158 | 159 | ╚ ╝ 160 | */ 161 | @include radiusRight($value); 162 | @include radiusBL($value); 163 | } 164 | 165 | @mixin radiusAllButBR($value: $defaultRadius) { 166 | /* 167 | ╔ ╗ 168 | 169 | ╚ ┘ 170 | */ 171 | @include radiusLeft($value); 172 | @include radiusTR($value); 173 | } 174 | 175 | @mixin radiusAllButBL($value: $defaultRadius) { 176 | /* 177 | ╔ ╗ 178 | 179 | └ ╝ 180 | */ 181 | @include radiusRight($value); 182 | @include radiusBR($value); 183 | } 184 | -------------------------------------------------------------------------------- /dev/scss/mixins/index.scss: -------------------------------------------------------------------------------- 1 | @import "browser"; 2 | @import "margin"; 3 | @import "padding"; 4 | @import "radius"; 5 | -------------------------------------------------------------------------------- /dev/views/.partials/layout.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% block title %}{% endblock %} 8 | 9 | {% include module("favicons") %} 10 | 11 | {% include module("globalCSS") %} 12 | {% block css %}{% endblock %} 13 | 14 | {% include module("globalJS") %} 15 | {% block js %}{% endblock %} 16 | 17 | 18 | {% include module("flash") %} 19 | {% include module("demo") %} 20 |
21 | {% block body %}{% endblock %} 22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /dev/views/.partials/modules/demo.twig: -------------------------------------------------------------------------------- 1 | {% macro navLink(text, name, param = {}, qs = {}) %} 2 | {% if is_current_url(name, param, qs) %} 3 | {{ text }} 4 | {% else %} 5 | 6 | {{ text }} 7 | 8 | {% endif %} 9 | {% endmacro %} 10 | 11 | {% from module("demo") import navLink %} 12 | 13 | {% set routes = [ 14 | { 15 | text: 'Demo', 16 | name: 'home', 17 | param: {} 18 | }, 19 | { 20 | text: 'Demo path parameter binding', 21 | name: 'demo2', 22 | param: { 23 | user: "iamuser" 24 | } 25 | }, 26 | { 27 | text: 'Force login (as "test")', 28 | name: 'auth.force_login', 29 | param: { 30 | __user: "test" 31 | } 32 | }, 33 | { 34 | text: 'Demo route-model binding (success for "test")', 35 | name: 'demo3', 36 | param: { 37 | __user: "test" 38 | } 39 | }, 40 | { 41 | text: 'Demo route-model binding (failure for "does_not_exist")', 42 | name: 'demo3', 43 | param: { 44 | __user: "does_not_exist" 45 | } 46 | }, 47 | { 48 | text: 'Routes that accept redirects : /redirectable?redir=https://google.fr', 49 | name: 'redirectable', 50 | qs: { 51 | redir: "https://google.fr" 52 | } 53 | }, 54 | { 55 | text: 'QR code (needs auth as "test")', 56 | name: 'demo.2fa', 57 | param: { 58 | __user: "test" 59 | } 60 | }, 61 | ] %} 62 | 63 |
64 | 73 |
74 | -------------------------------------------------------------------------------- /dev/views/.partials/modules/favicons.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /dev/views/.partials/modules/flash.twig: -------------------------------------------------------------------------------- 1 | {% for type in ["failure", "success", "info"] %} 2 | {% for msg in flash(type) %} 3 |
{# jq-flash #} 4 | 5 |

{{ msg }}

6 |
7 | {% endfor %} 8 | {% endfor %} -------------------------------------------------------------------------------- /dev/views/.partials/modules/globalCSS.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {##} 6 | -------------------------------------------------------------------------------- /dev/views/.partials/modules/globalJS.twig: -------------------------------------------------------------------------------- 1 | {# 2 | 3 | #} 4 | 5 | 6 | {##} 7 | -------------------------------------------------------------------------------- /dev/views/2fa.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block title %}Homepage{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block js %} 10 | 11 | {% endblock %} 12 | 13 | {% block body %} 14 |

15 | Enable 2FA 16 | Disable 2FA 17 |

18 | 19 | Use 2FA for {{ user.username }} at {{ user.email }}
20 | 21 | 22 | 23 |

24 | Secret :
25 | {{ secret }} 26 |

27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /dev/views/auth/login.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block title %}Login{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 | 10 |

11 |
12 | 13 |
14 | 15 | 16 |

17 |
18 | 19 |
20 | 21 | 22 |

23 |
24 | 25 |
26 | 27 | 28 |
29 | 30 | 33 | 34 | 35 | You don't have an account? Register 36 | 37 | 38 | {{ csrf_input() }} 39 |
40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /dev/views/auth/register.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block title %}Login{% endblock %} 4 | 5 | {% block body %} 6 |
7 |
8 | 9 | 10 |

11 |
12 | 13 | 14 |
15 | 16 | 17 |

18 |
19 | 20 |
21 | 22 | 23 |

24 |
25 | 26 |
27 | 28 | 29 |

30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 | 40 | 41 | {{ csrf_input() }} 42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /dev/views/demo.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block title %}Homepage{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block js %} 10 | 11 | {% endblock %} 12 | 13 | {% block body %} 14 | {{ json }} 15 | 16 | Hello World!
17 | This is a demo page
18 | {{ phpver }}
19 | 20 | 21 | {% if user %} 22 | Hello user #{{ user.id }} named "{{ user.username }}" 23 | Log out 24 | {% else %} 25 | Log in 26 | {% endif %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /dev/views/demo2.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block title %} Homepage {% endblock %} 4 | 5 | {% block body %} 6 | Hello World!
7 | Hello {{ user }} ! 8 | {% endblock %} -------------------------------------------------------------------------------- /dev/views/demo3.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block title %}View Model Binding{% endblock %} 4 | 5 | {% block body %} 6 | User #{{ user.id }} 7 | {{ dump(user) }} 8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /dev/views/errors/400.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block body %} 4 |

Error 400 (Bad Request)

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /dev/views/errors/401.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block body %} 4 |

Error 401 (Unauthorized)

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /dev/views/errors/403.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block body %} 4 |

Error 403 (Forbidden)

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /dev/views/errors/404.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block body %} 4 |

Error 404 (Not Found)

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /dev/views/errors/500.twig: -------------------------------------------------------------------------------- 1 | {% extends layout() %} 2 | 3 | {% block body %} 4 |

Error 500 (Internal Server Error)

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /dev/vue/components/Demo.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | 18 | 24 | -------------------------------------------------------------------------------- /dev/vue/components/Hamburger.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 72 | 73 | 76 | -------------------------------------------------------------------------------- /dev/vue/components/QrCode.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 19 | -------------------------------------------------------------------------------- /dev/vue/index.js: -------------------------------------------------------------------------------- 1 | import { sync } from "vuex-router-sync"; 2 | import { Env } from "@js/utils"; 3 | import pluginsInstaller from "./plugins"; 4 | import Vue from "$vue"; 5 | 6 | Vue.config.productionTip = Env.dev || Env.test; 7 | pluginsInstaller(Vue); 8 | 9 | const newVueInstance = options => { 10 | const unsyncRouterStore = sync( 11 | options.store, options.router, { moduleName: "router" } 12 | ); 13 | 14 | const vm = new Vue(options); 15 | 16 | return { 17 | vm, 18 | unsyncRouterStore, 19 | }; 20 | }; 21 | 22 | export { 23 | Vue, 24 | newVueInstance, 25 | }; 26 | -------------------------------------------------------------------------------- /dev/vue/plugins/flash.js: -------------------------------------------------------------------------------- 1 | import { flash } from "vanilla_flash"; 2 | 3 | /** 4 | * @param {import("vue").VueConstructor} Vue 5 | */ 6 | export default function vanillaFlash(Vue){ 7 | Object.defineProperties(Vue.prototype, { 8 | $flash: { 9 | get(){ 10 | return flash; 11 | }, 12 | }, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /dev/vue/plugins/index.js: -------------------------------------------------------------------------------- 1 | import vueRouter from "./vue-router"; 2 | import vuex from "./vuex"; 3 | import indexedDb from "./indexedDB"; 4 | import flash from "./flash"; 5 | import json from "./json"; 6 | import ls from "./localStorage"; 7 | import mediaQueries from "./mediaQueries"; 8 | 9 | /** 10 | * Installer function for the Vue plugins 11 | * @param {Vue | VueConstructor} Vue 12 | */ 13 | export default function pluginsInstaller(Vue){ 14 | /// PLUGINS 15 | const factories = [ 16 | vueRouter, 17 | vuex, 18 | indexedDb, 19 | flash, 20 | json, 21 | ls, 22 | mediaQueries, 23 | ]; 24 | 25 | factories.forEach(factory => factory(Vue)); 26 | } 27 | -------------------------------------------------------------------------------- /dev/vue/plugins/indexedDB.js: -------------------------------------------------------------------------------- 1 | import db from "db.js"; 2 | 3 | /** 4 | * @var {DbJs.OpenOptions} config - The database connection options 5 | */ 6 | const config = { 7 | server: "MyDB", 8 | version: 1, 9 | schema: { 10 | people: { 11 | key: { 12 | keyPath: "id", 13 | autoIncrement: true, 14 | }, 15 | indexes: { 16 | firstName: {}, 17 | answer: { unique: true }, 18 | }, 19 | }, 20 | }, 21 | }; 22 | 23 | /** 24 | * Factory installer for the indexed DB client 25 | * @param {import("vue").VueConstructor} Vue 26 | */ 27 | export default async function indexedDb(Vue){ 28 | const $connection = await db.open(config); 29 | 30 | const plugin = { 31 | install(/* Vue */){ 32 | Object.defineProperties(Vue.prototype, { 33 | $db: { 34 | get(){ 35 | return $connection; 36 | }, 37 | }, 38 | }); 39 | }, 40 | }; 41 | 42 | Vue.use(plugin); 43 | } 44 | -------------------------------------------------------------------------------- /dev/vue/plugins/json.js: -------------------------------------------------------------------------------- 1 | import { $json } from "@voltra/json"; 2 | 3 | /** 4 | * @param {import("vue").VueConstructor} Vue 5 | */ 6 | export default function json(Vue){ 7 | Object.defineProperties(Vue.prototype, { 8 | $json: { 9 | get(){ 10 | return $json; 11 | }, 12 | }, 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /dev/vue/plugins/localStorage.js: -------------------------------------------------------------------------------- 1 | import $ls from "store"; 2 | 3 | /** 4 | * @param {import("vue").VueConstructor} Vue 5 | */ 6 | export default function localStorage(Vue){ 7 | Object.defineProperties(Vue.prototype, { 8 | $localStorage: { 9 | get(){ 10 | return $ls; 11 | }, 12 | }, 13 | $ls: { 14 | get(){ 15 | return $ls; 16 | }, 17 | }, 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /dev/vue/plugins/mediaQueries.js: -------------------------------------------------------------------------------- 1 | import VueMq from "vue-mq"; 2 | 3 | /** 4 | * @param {import("vue").VueConstructor} Vue 5 | */ 6 | export default function mediaQueries(Vue){ 7 | Vue.use(VueMq, { 8 | xsmall: 540, // up to 540 => xsmall 9 | small: 900, // from 540 up to 900 => small 10 | medium: 1280, 11 | large: 1440, 12 | xlarge: 1920, 13 | xxlarge: Infinity, 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /dev/vue/plugins/vue-router.js: -------------------------------------------------------------------------------- 1 | import VueRouter from "vue-router"; 2 | 3 | /** 4 | * @param {import("vue").VueConstructor} Vue 5 | */ 6 | export default function vueRouter(Vue){ 7 | Vue.use(VueRouter); 8 | } 9 | -------------------------------------------------------------------------------- /dev/vue/plugins/vuex.js: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex"; 2 | 3 | /** 4 | * @param {import("vue").VueConstructor} Vue 5 | */ 6 | export default function vuex(Vue){ 7 | Vue.use(Vuex); 8 | } 9 | -------------------------------------------------------------------------------- /dev/vue/router/index.js: -------------------------------------------------------------------------------- 1 | import VueRouter from "vue-router"; 2 | import Route from "vue-routisan/src"; 3 | 4 | export default new VueRouter({ 5 | mode: "history", 6 | routes: Route.all(), 7 | linkActiveClass: "active", 8 | linkExactActiveClass: "active--exact", 9 | scrollBehavior( 10 | to, 11 | from, 12 | savedPosition 13 | ){ 14 | return savedPosition 15 | ? savedPosition 16 | : { 17 | x: 0, 18 | y: 0, 19 | }; 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /dev/vue/store/index.js: -------------------------------------------------------------------------------- 1 | 2 | import Vuex from "vuex"; 3 | import pathify from "vuex-pathify"; 4 | import counter from "./modules/counter"; 5 | 6 | /** 7 | * @type {import("vuex").StoreOptions} 8 | */ 9 | const store = { modules: { counter } }; 10 | 11 | export default new Vuex.Store({ 12 | ...store, 13 | plugins: [pathify.plugin], 14 | }); 15 | -------------------------------------------------------------------------------- /dev/vue/store/modules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/dev/vue/store/modules/.gitkeep -------------------------------------------------------------------------------- /dev/vue/store/modules/counter/actions.js: -------------------------------------------------------------------------------- 1 | import { make } from "vuex-pathify"; 2 | import state from "./state"; 3 | 4 | /** 5 | * @type {import("vuex").ActionTree} 6 | */ 7 | export default{ 8 | ...make.actions(state), 9 | 10 | /** 11 | * @property {import("vuex").Action} loadCount 12 | */ 13 | loadCount({ commit }){ 14 | fetch("https://google.fr") 15 | .then(() => commit("setCount", 200)) 16 | .catch(() => commit("setCount", -1)); 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /dev/vue/store/modules/counter/getters.js: -------------------------------------------------------------------------------- 1 | import { make } from "vuex-pathify"; 2 | import state from "./state"; 3 | 4 | /** 5 | * @type {import("vuex").GetterTree} 6 | */ 7 | export default{ 8 | ...make.getters(state), 9 | /** 10 | * @property {import("vuex").Getter} next 11 | * @param {typeof state} state - The current state 12 | */ 13 | next(state){ 14 | return state.count; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /dev/vue/store/modules/counter/index.js: -------------------------------------------------------------------------------- 1 | import { makeModule } from "@/vue/store/utils"; 2 | import state from "./state"; 3 | import getters from "./getters"; 4 | import mutations from "./mutations"; 5 | import actions from "./actions"; 6 | 7 | /* 8 | oneState 9 | oneGetter 10 | ONE_MUTATION 11 | oneAction 12 | */ 13 | 14 | export default makeModule({ 15 | state, 16 | getters, 17 | mutations, 18 | actions, 19 | }); 20 | -------------------------------------------------------------------------------- /dev/vue/store/modules/counter/mutations.js: -------------------------------------------------------------------------------- 1 | import { make } from "vuex-pathify"; 2 | import state from "./state"; 3 | 4 | /** 5 | * @type {import("vuex").MutationTree} 6 | */ 7 | export default{ 8 | ...make.mutations(state), 9 | 10 | /** 11 | * @property {import("vuex").Mutation} INCREMENT 12 | * @param {typeof state} state - The current state 13 | */ 14 | INCREMENT(state){ 15 | state.count += 1; 16 | }, 17 | 18 | /** 19 | * @property {import("vuex").Mutation} DECREMENT 20 | * @param {typeof state} state - The current state 21 | */ 22 | DECREMENT(state){ 23 | state.count -= 1; 24 | }, 25 | 26 | /** 27 | * @property {import("vuex").Mutation} RESET 28 | * @param {typeof state} state - The current state 29 | */ 30 | RESET(state){ 31 | state.count = state.defaultCount; 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /dev/vue/store/modules/counter/state.js: -------------------------------------------------------------------------------- 1 | const defaultCount = 0; 2 | 3 | export default{ 4 | count: defaultCount, 5 | defaultCount, 6 | }; 7 | -------------------------------------------------------------------------------- /dev/vue/store/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Make a module from its definition 3 | * @param {import("vuex").Module} options - The options for the module 4 | * @returns {import("vuex").Module} 5 | */ 6 | const makeModule = options => ({ 7 | ...options, 8 | namespaced: true, 9 | }); 10 | 11 | export { makeModule }; 12 | -------------------------------------------------------------------------------- /dev/vue/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a lazy loaded component wrapper 3 | * @param {string} path - The path to the SFC (relative to the components root) 4 | * @returns {function(): (Promise<*>|*)} 5 | */ 6 | const lazyc = path => () => import(`@components/${path}.vue`); 7 | 8 | export { lazyc }; 9 | -------------------------------------------------------------------------------- /eslint-rules/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/eslint-rules/.gitkeep -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "slim-vue-app", 3 | "version": "2.1.0", 4 | "description": "An application that uses Slim and Vue (with Twig)", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "npm run watch", 8 | "test": "npm run test:php && npm run test:js", 9 | "lint": "eslint . --fix", 10 | "build": "webpack", 11 | "watch": "webpack --watch", 12 | "setup": "npm run setup:cli && npm run setup:js && npm run setup:php && npm run setup:routes", 13 | "test:php": "./vendor/bin/phpunit --testdox", 14 | "test:js": "npm run test:unit:js && npm run test:e2e", 15 | "test:unit:js": "jest --colors", 16 | "test:e2e": "cypress run", 17 | "setup:routes": "source aliases.sh && pushd dev && ./dumpRouteLoader.sh && popd", 18 | "setup:cli": "dbmate -h || go get -u github.com/amacneil/dbmate", 19 | "setup:php": "composer update && composer dump-autoload", 20 | "setup:js": "npm i && npm audit fix" 21 | }, 22 | "author": "Voltra ", 23 | "license": "MIT", 24 | "engines": { 25 | "node": ">=10.13.0" 26 | }, 27 | "devDependencies": { 28 | "@babel/cli": "^7.11.6", 29 | "@babel/core": "^7.11.6", 30 | "@babel/parser": "7.7.5", 31 | "@babel/plugin-proposal-class-properties": "^7.10.4", 32 | "@babel/plugin-proposal-do-expressions": "^7.10.4", 33 | "@babel/plugin-proposal-function-bind": "^7.11.5", 34 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", 35 | "@babel/plugin-proposal-numeric-separator": "^7.10.4", 36 | "@babel/plugin-proposal-object-rest-spread": "^7.11.0", 37 | "@babel/plugin-proposal-optional-chaining": "^7.11.0", 38 | "@babel/plugin-proposal-partial-application": "^7.10.5", 39 | "@babel/plugin-proposal-pipeline-operator": "^7.10.5", 40 | "@babel/plugin-proposal-throw-expressions": "^7.10.4", 41 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 42 | "@babel/plugin-syntax-jsx": "^7.10.4", 43 | "@babel/preset-env": "^7.11.5", 44 | "@babel/types": "^7.11.5", 45 | "@cypress/webpack-preprocessor": "^5.4.5", 46 | "@types/jest": "^26.0.13", 47 | "@vue/babel-preset-jsx": "^1.1.2", 48 | "@vue/test-utils": "^1.1.0", 49 | "autoprefixer": "^9.8.6", 50 | "babel-eslint": "^10.1.0", 51 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 52 | "babel-jest": "^26.3.0", 53 | "babel-loader": "^8.1.0", 54 | "babel-plugin-jsx-v-model": "^2.0.3", 55 | "babel-plugin-transform-jsx": "^2.0.0", 56 | "babel-plugin-transform-vue-jsx": "^3.7.0", 57 | "clean-webpack-plugin": "^3.0.0", 58 | "compression-webpack-plugin": "^5.0.2", 59 | "copy-webpack-plugin": "^6.1.0", 60 | "core-js": "^3.6.5", 61 | "css-loader": "^4.3.0", 62 | "css-mquery-packer": "^1.2.4", 63 | "cssnano": "^4.1.10", 64 | "cypress": "^5.1.0", 65 | "dotenv": "^8.2.0", 66 | "dotenv-expand": "^5.1.0", 67 | "dotenv-safe": "^8.2.0", 68 | "dotenv-webpack": "^2.0.0", 69 | "eslint": "^7.8.1", 70 | "eslint-loader": "^4.0.2", 71 | "eslint-plugin-babel": "^5.3.1", 72 | "eslint-plugin-import": "^2.22.0", 73 | "eslint-plugin-rulesdir": "^0.1.0", 74 | "eslint-rule-composer": "^0.3.0", 75 | "favicons-webpack-plugin": "^4.2.0", 76 | "file-loader": "^6.1.0", 77 | "friendly-errors-webpack-plugin": "^1.7.0", 78 | "img-loader": "^3.0.1", 79 | "jest": "^26.4.2", 80 | "mini-css-extract-plugin": "^0.11.1", 81 | "node-sass": "^4.14.1", 82 | "postcss-cssnext": "^3.1.0", 83 | "postcss-import": "^12.0.1", 84 | "postcss-loader": "^4.0.1", 85 | "postcss-preset-env": "^6.7.0", 86 | "sass-loader": "^10.0.2", 87 | "script-loader": "^0.7.2", 88 | "style-loader": "^1.2.1", 89 | "url-loader": "^4.1.0", 90 | "vue-jest": "^3.0.6", 91 | "vue-loader": "^15.9.3", 92 | "vue-mq": "^1.0.1", 93 | "vue-template-compiler": "^2.6.12", 94 | "webpack": "^4.44.1", 95 | "webpack-cli": "^3.3.12", 96 | "webpack-external-svg-sprite": "^1.0.0", 97 | "webpack-manifest-plugin": "^2.2.0", 98 | "webpack-progress-bar": "^1.2.1" 99 | }, 100 | "dependencies": { 101 | "@voltra/json": "^3.0.1", 102 | "color-hash": "^1.0.3", 103 | "compary": "^0.1.0", 104 | "db.js": "^0.15.0", 105 | "hamburgers": "^1.1.3", 106 | "sass-mq": "^5.0.1", 107 | "sequency": "^0.19.2", 108 | "spinner-lord": "^0.2.6", 109 | "store": "^2.0.12", 110 | "vanilla_flash": "^2.0.1", 111 | "vue": "^2.6.12", 112 | "vue-router": "^3.4.3", 113 | "vue-routisan": "^2.1.4", 114 | "vue-types": "^2.0.1", 115 | "vuex": "^3.5.1", 116 | "vuex-pathify": "^1.4.1", 117 | "vuex-router-sync": "^5.0.0" 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | ./tests/Demo/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | 3 | module.exports = ({ file, env }) => ({ 4 | plugins: { 5 | // "postcss-cssnext": {}, // Replaced by postcss-preset-env 6 | autoprefixer: { remove: false }, //keep legacy prefixes 7 | cssnano: env === "production" ? {} : false, 8 | "css-mquery-packer": {}, // Group media queries 9 | "postcss-preset-env": {}, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /public_html/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine On 2 | RewriteCond %{REQUEST_FILENAME} !-f 3 | RewriteRule ^ index.php [QSA,L] 4 | -------------------------------------------------------------------------------- /public_html/assets/css/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/css/.gitkeep -------------------------------------------------------------------------------- /public_html/assets/css/clear_float.css: -------------------------------------------------------------------------------- 1 | /*because we needa clear it boi*/ 2 | div.clear_float{ 3 | clear:both; 4 | width:0; 5 | height:0; 6 | } 7 | 8 | 9 | /*test*/ 10 | clear_float{ 11 | clear:both; 12 | width:0; 13 | height:0; 14 | } -------------------------------------------------------------------------------- /public_html/assets/css/demo.css: -------------------------------------------------------------------------------- 1 | .demo[data-v-6e3e423b] { 2 | background-color: aqua; 3 | color: green; 4 | } 5 | 6 | /*! 7 | * Hamburgers 8 | * @description Tasty CSS-animated hamburgers 9 | * @author Jonathan Suh @jonsuh 10 | * @site https://jonsuh.com/hamburgers 11 | * @link https://github.com/jonsuh/hamburgers 12 | */ 13 | .hamburger[data-v-bc609a3e] { 14 | padding: 15px 15px; 15 | display: inline-block; 16 | cursor: pointer; 17 | transition-property: opacity, filter; 18 | transition-duration: 0.15s; 19 | transition-timing-function: linear; 20 | font: inherit; 21 | color: inherit; 22 | text-transform: none; 23 | background-color: transparent; 24 | border: 0; 25 | margin: 0; 26 | overflow: visible; 27 | } 28 | .hamburger[data-v-bc609a3e]:hover { 29 | opacity: 0.7; 30 | } 31 | .hamburger.is-active[data-v-bc609a3e]:hover { 32 | opacity: 0.7; 33 | } 34 | .hamburger.is-active .hamburger-inner[data-v-bc609a3e], 35 | .hamburger.is-active .hamburger-inner[data-v-bc609a3e]::before, 36 | .hamburger.is-active .hamburger-inner[data-v-bc609a3e]::after { 37 | background-color: black; 38 | } 39 | .hamburger-box[data-v-bc609a3e] { 40 | width: 40px; 41 | height: 24px; 42 | display: inline-block; 43 | position: relative; 44 | } 45 | .hamburger-inner[data-v-bc609a3e] { 46 | display: block; 47 | top: 50%; 48 | margin-top: -2px; 49 | } 50 | .hamburger-inner[data-v-bc609a3e], .hamburger-inner[data-v-bc609a3e]::before, .hamburger-inner[data-v-bc609a3e]::after { 51 | width: 40px; 52 | height: 4px; 53 | background-color: black; 54 | border-radius: 4px; 55 | position: absolute; 56 | transition-property: transform; 57 | transition-duration: 0.15s; 58 | transition-timing-function: ease; 59 | } 60 | .hamburger-inner[data-v-bc609a3e]::before, .hamburger-inner[data-v-bc609a3e]::after { 61 | content: ""; 62 | display: block; 63 | } 64 | .hamburger-inner[data-v-bc609a3e]::before { 65 | top: -10px; 66 | } 67 | .hamburger-inner[data-v-bc609a3e]::after { 68 | bottom: -10px; 69 | } 70 | 71 | /* 72 | * Emphatic 73 | */ 74 | .hamburger--emphatic[data-v-bc609a3e] { 75 | overflow: hidden; 76 | } 77 | .hamburger--emphatic .hamburger-inner[data-v-bc609a3e] { 78 | transition: background-color 0.125s 0.175s ease-in; 79 | } 80 | .hamburger--emphatic .hamburger-inner[data-v-bc609a3e]::before { 81 | left: 0; 82 | transition: transform 0.125s cubic-bezier(0.6, 0.04, 0.98, 0.335), top 0.05s 0.125s linear, left 0.125s 0.175s ease-in; 83 | } 84 | .hamburger--emphatic .hamburger-inner[data-v-bc609a3e]::after { 85 | top: 10px; 86 | right: 0; 87 | transition: transform 0.125s cubic-bezier(0.6, 0.04, 0.98, 0.335), top 0.05s 0.125s linear, right 0.125s 0.175s ease-in; 88 | } 89 | .hamburger--emphatic.is-active .hamburger-inner[data-v-bc609a3e] { 90 | transition-delay: 0s; 91 | transition-timing-function: ease-out; 92 | background-color: transparent !important; 93 | } 94 | .hamburger--emphatic.is-active .hamburger-inner[data-v-bc609a3e]::before { 95 | left: -80px; 96 | top: -80px; 97 | transform: translate3d(80px, 80px, 0) rotate(45deg); 98 | transition: left 0.125s ease-out, top 0.05s 0.125s linear, transform 0.125s 0.175s cubic-bezier(0.075, 0.82, 0.165, 1); 99 | } 100 | .hamburger--emphatic.is-active .hamburger-inner[data-v-bc609a3e]::after { 101 | right: -80px; 102 | top: -80px; 103 | transform: translate3d(-80px, 80px, 0) rotate(-45deg); 104 | transition: right 0.125s ease-out, top 0.05s 0.125s linear, transform 0.125s 0.175s cubic-bezier(0.075, 0.82, 0.165, 1); 105 | } 106 | .hamburger[data-v-bc609a3e] { 107 | position: fixed; 108 | top: 0; 109 | right: 0; 110 | z-index: 20; 111 | display: block; 112 | outline: none; 113 | opacity: 0.9; 114 | } 115 | .hamburger[data-v-bc609a3e]:hover { 116 | opacity: 1; 117 | } 118 | 119 | -------------------------------------------------------------------------------- /public_html/assets/css/demo.css.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/css/demo.css.gz -------------------------------------------------------------------------------- /public_html/assets/css/font_loader.css: -------------------------------------------------------------------------------- 1 | /*********TEMPLATE**********\ 2 | @font-face{ 3 | font-family: name; 4 | src: url(""); 5 | } 6 | \***************************/ 7 | 8 | /*///////////////////////////////////////////////////////////////*/ 9 | 10 | @font-face{ 11 | font-family: raleway; 12 | src: url("/assets/fonts/raleway.ttf"); 13 | } 14 | 15 | @font-face{ 16 | font-family: roboto; 17 | src: url("/assets/fonts/roboto.ttf"); 18 | } -------------------------------------------------------------------------------- /public_html/assets/css/reset.css: -------------------------------------------------------------------------------- 1 | /* 2 | html5doctor.com Reset Stylesheet 3 | v1.6.1 4 | Last Updated: 2010-09-17 5 | Author: Richard Clark - http://richclarkdesign.com 6 | Twitter: @rich_clark 7 | */ 8 | 9 | html, body, div, span, object, iframe, 10 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 11 | abbr, address, cite, code, 12 | del, dfn, em, img, ins, kbd, q, samp, 13 | small, strong, sub, sup, var, 14 | b, i, 15 | dl, dt, dd, ol, ul, li, 16 | fieldset, form, label, legend, 17 | table, caption, tbody, tfoot, thead, tr, th, td, 18 | article, aside, canvas, details, figcaption, figure, 19 | footer, header, hgroup, menu, nav, section, summary, 20 | time, mark, audio, video { 21 | margin:0; 22 | padding:0; 23 | border:0; 24 | outline:0; 25 | font-size:100%; 26 | vertical-align:baseline; 27 | background:transparent; 28 | } 29 | 30 | body { 31 | line-height:1; 32 | } 33 | 34 | article,aside,details,figcaption,figure, 35 | footer,header,hgroup,menu,nav,section { 36 | display:block; 37 | } 38 | 39 | nav ul { 40 | list-style:none; 41 | } 42 | 43 | blockquote, q { 44 | quotes:none; 45 | } 46 | 47 | blockquote:before, blockquote:after, 48 | q:before, q:after { 49 | content:''; 50 | content:none; 51 | } 52 | 53 | a { 54 | margin:0; 55 | padding:0; 56 | font-size:100%; 57 | vertical-align:baseline; 58 | background:transparent; 59 | } 60 | 61 | /* change colours to suit your needs */ 62 | ins { 63 | background-color:transparent; 64 | color:#000; 65 | text-decoration:none; 66 | } 67 | 68 | /* change colours to suit your needs */ 69 | mark { 70 | background-color:transparent; 71 | color:#000; 72 | font-style:italic; 73 | font-weight:bold; 74 | } 75 | 76 | del { 77 | text-decoration: line-through; 78 | } 79 | 80 | abbr[title], dfn[title] { 81 | border-bottom:1px dotted; 82 | cursor:help; 83 | } 84 | 85 | table { 86 | border-collapse:collapse; 87 | border-spacing:0; 88 | } 89 | 90 | /* change border colour to suit your needs */ 91 | hr { 92 | display:block; 93 | height:1px; 94 | border:0; 95 | border-top:1px solid #000; 96 | margin:1em 0; 97 | padding:0; 98 | } 99 | 100 | input, select { 101 | vertical-align:middle; 102 | } -------------------------------------------------------------------------------- /public_html/assets/fonts/raleway.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/fonts/raleway.ttf -------------------------------------------------------------------------------- /public_html/assets/fonts/roboto.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/fonts/roboto.ttf -------------------------------------------------------------------------------- /public_html/assets/img/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/img/.gitkeep -------------------------------------------------------------------------------- /public_html/assets/img/favicons/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/img/favicons/.gitkeep -------------------------------------------------------------------------------- /public_html/assets/img/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/img/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public_html/assets/img/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/img/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public_html/assets/img/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/img/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public_html/assets/img/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/img/favicons/favicon.ico -------------------------------------------------------------------------------- /public_html/assets/img/favicons/favicon.ico.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/img/favicons/favicon.ico.gz -------------------------------------------------------------------------------- /public_html/assets/js/2fa.bundle.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/js/2fa.bundle.js.gz -------------------------------------------------------------------------------- /public_html/assets/js/demo.bundle.js.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/assets/js/demo.bundle.js.gz -------------------------------------------------------------------------------- /public_html/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "2fa.js": "/assets/js/2fa.bundle.js", 3 | "demo.css": "/assets/css/demo.css", 4 | "demo.js": "/assets/js/demo.bundle.js", 5 | "css/demo.css.gz": "/assets/css/demo.css.gz", 6 | "img/favicons/apple-touch-icon.png": "/assets/img/favicons/apple-touch-icon.png", 7 | "img/favicons/favicon-16x16.png": "/assets/img/favicons/favicon-16x16.png", 8 | "img/favicons/favicon-32x32.png": "/assets/img/favicons/favicon-32x32.png", 9 | "img/favicons/favicon.ico": "/assets/img/favicons/favicon.ico", 10 | "img/favicons/favicon.ico.gz": "/assets/img/favicons/favicon.ico.gz", 11 | "js/2fa.bundle.js.gz": "/assets/js/2fa.bundle.js.gz", 12 | "js/demo.bundle.js.gz": "/assets/js/demo.bundle.js.gz" 13 | } -------------------------------------------------------------------------------- /public_html/index.php: -------------------------------------------------------------------------------- 1 | run(); 5 | -------------------------------------------------------------------------------- /public_html/uploads/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voltra/slim-vue-app/ac50fb8644f5160fb4a694be1128c37bb7d49532/public_html/uploads/.gitkeep -------------------------------------------------------------------------------- /public_html/uploads/demo.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "world" 3 | } 4 | --------------------------------------------------------------------------------