├── .bowerrc ├── .editorconfig ├── .gitignore ├── README.md ├── bower.json ├── config.xml ├── gulpfile.js ├── hooks ├── README.md └── after_prepare │ └── 010_add_platform_class.js ├── ionic.project ├── package.json ├── resources ├── android │ ├── icon │ │ ├── drawable-hdpi-icon.png │ │ ├── drawable-ldpi-icon.png │ │ ├── drawable-mdpi-icon.png │ │ ├── drawable-xhdpi-icon.png │ │ ├── drawable-xxhdpi-icon.png │ │ └── drawable-xxxhdpi-icon.png │ └── splash │ │ ├── drawable-land-hdpi-screen.png │ │ ├── drawable-land-ldpi-screen.png │ │ ├── drawable-land-mdpi-screen.png │ │ ├── drawable-land-xhdpi-screen.png │ │ ├── drawable-land-xxhdpi-screen.png │ │ ├── drawable-land-xxxhdpi-screen.png │ │ ├── drawable-port-hdpi-screen.png │ │ ├── drawable-port-ldpi-screen.png │ │ ├── drawable-port-mdpi-screen.png │ │ ├── drawable-port-xhdpi-screen.png │ │ ├── drawable-port-xxhdpi-screen.png │ │ └── drawable-port-xxxhdpi-screen.png ├── icon.png ├── ios │ ├── icon │ │ ├── icon-40.png │ │ ├── icon-40@2x.png │ │ ├── icon-50.png │ │ ├── icon-50@2x.png │ │ ├── icon-60.png │ │ ├── icon-60@2x.png │ │ ├── icon-60@3x.png │ │ ├── icon-72.png │ │ ├── icon-72@2x.png │ │ ├── icon-76.png │ │ ├── icon-76@2x.png │ │ ├── icon-small.png │ │ ├── icon-small@2x.png │ │ ├── icon-small@3x.png │ │ ├── icon.png │ │ └── icon@2x.png │ └── splash │ │ ├── Default-568h@2x~iphone.png │ │ ├── Default-667h.png │ │ ├── Default-736h.png │ │ ├── Default-Landscape-736h.png │ │ ├── Default-Landscape@2x~ipad.png │ │ ├── Default-Landscape~ipad.png │ │ ├── Default-Portrait@2x~ipad.png │ │ ├── Default-Portrait~ipad.png │ │ ├── Default@2x~iphone.png │ │ └── Default~iphone.png └── splash.png ├── scss └── ionic.app.scss └── www ├── img └── ionic.png ├── index.html ├── js ├── app.js ├── controllers.js └── services.js └── templates ├── app └── user.html └── auth ├── login.html └── signup.html /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "www/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | node_modules/ 5 | platforms/ 6 | plugins/ 7 | 8 | www/lib/ 9 | www/css/ 10 | 11 | .DS_Store 12 | ._DS_Store 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Add Firebase authentication to Ionic Apps 2 | This repo is part of an Ionic Firebase Tutorial where you can learn how to add Firebase authentication to an Ionic Framework App. 3 | 4 | As an example we are going to build a simple app that allows users to login and signup to your app using Firebase authentication services. Once they log in, they will see a home page with their basic profile info. 5 | 6 | ### *Important note*: this tutorial is for Ionic v1. [Get the updated tutorial and example app built with Ionic 5 and Firebase](https://ionicthemes.com/tutorials/about/firebase-authentication-in-ionic-framework-apps). 7 | 8 | Check all the details of this demo app in the step by step [Ionic 1 Firebase Auth Tutorial](https://ionicthemes.com/tutorials/about/add-firebase-authentication-to-your-ionic-app) 9 | 10 | ## Setup 11 | 12 | ### Install Ionic 13 | You can find the **_Ionic_** official installation documentation [here](http://ionicframework.com/docs/guide/installation.html). 14 | 15 | 1. Make sure you have an up-to-date version of **_Node.js_** installed on your system. If you don't have **_Node.js_** installed, you can install it from [here](http://nodejs.org/). 16 | 2. Open a terminal (Mac) or a command interpreter (`cmd`, Windows), and install **_Cordova_** and **_Ionic_**: 17 | - `npm install -g cordova` 18 | - `npm install -g ionic` 19 | - On a Mac, you may have to use `sudo` depending on your system configuration: `sudo npm install -g cordova ionic` 20 | 3. If you already have **_Cordova_** and **_Ionic_** installed on your computer, make sure you update to the latest version: 21 | - `npm update -g cordova` 22 | - `npm update -g ionic` 23 | - Or `sudo npm update -g cordova ionic` 24 | 25 | Follow these links if you want more information: 26 | * [Ionic **_Getting started_** guide](http://ionicframework.com/getting-started) 27 | * [Ionic **_Documentation_**](http://ionicframework.com/docs) 28 | * [Visit the Ionic **_Community Forum_**](http://forum.ionicframework.com) 29 | 30 | ### Git & `ionic start` 31 | 32 | First we need to link this new Ionic project with our reference repo on github. Clone this repo so we can start working on the app: 33 | - `git clone https://github.com/ionicthemes/firebase-authentication-for-your-ionic-app.git` 34 | - `cd firebase-authentication-for-your-ionic-app` 35 | 36 | After this, we need to set up some stuff before starting working on the **_Ionic_** project. To do so, run these commands: 37 | - `npm install` 38 | - `bower install` 39 | 40 | Finally, to see the current state of the project, run: 41 | - `ionic serve` 42 | 43 | ### [Find lots of Ionic Tutorials and Starter Apps in IonicThemes](https://ionicthemes.com). 44 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Firebase Login", 3 | "private": "true", 4 | "homepage": "https://github.com/ionicthemes", 5 | "authors": [ 6 | "Gonza Di Giovanni " 7 | ], 8 | "devDependencies": { 9 | "ionic": "driftyco/ionic-bower#1.3.1" 10 | }, 11 | "dependencies": { 12 | "firebase": "^2.2.4", 13 | "angularfire": "^1.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Firebase Login 4 | 5 | An Ionic Framework and Cordova project. 6 | 7 | 8 | Gonza Di Giovanni 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 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gutil = require('gulp-util'); 3 | var bower = require('bower'); 4 | var concat = require('gulp-concat'); 5 | var sass = require('gulp-sass'); 6 | var minifyCss = require('gulp-minify-css'); 7 | var rename = require('gulp-rename'); 8 | var sh = require('shelljs'); 9 | 10 | var paths = { 11 | sass: ['./scss/**/*.scss'] 12 | }; 13 | 14 | gulp.task('default', ['sass']); 15 | 16 | gulp.task('sass', function(done) { 17 | gulp.src('./scss/ionic.app.scss') 18 | .pipe(sass()) 19 | .on('error', sass.logError) 20 | .pipe(gulp.dest('./www/css/')) 21 | .pipe(minifyCss({ 22 | keepSpecialComments: 0 23 | })) 24 | .pipe(rename({ extname: '.min.css' })) 25 | .pipe(gulp.dest('./www/css/')) 26 | .on('end', done); 27 | }); 28 | 29 | gulp.task('watch', function() { 30 | gulp.watch(paths.sass, ['sass']); 31 | }); 32 | 33 | gulp.task('install', ['git-check'], function() { 34 | return bower.commands.install() 35 | .on('log', function(data) { 36 | gutil.log('bower', gutil.colors.cyan(data.id), data.message); 37 | }); 38 | }); 39 | 40 | gulp.task('git-check', function(done) { 41 | if (!sh.which('git')) { 42 | console.log( 43 | ' ' + gutil.colors.red('Git is not installed.'), 44 | '\n Git, the version control system, is required to download Ionic.', 45 | '\n Download git here:', gutil.colors.cyan('http://git-scm.com/downloads') + '.', 46 | '\n Once git is installed, run \'' + gutil.colors.cyan('gulp install') + '\' again.' 47 | ); 48 | process.exit(1); 49 | } 50 | done(); 51 | }); 52 | -------------------------------------------------------------------------------- /hooks/README.md: -------------------------------------------------------------------------------- 1 | 21 | # Cordova Hooks 22 | 23 | This directory may contain scripts used to customize cordova commands. This 24 | directory used to exist at `.cordova/hooks`, but has now been moved to the 25 | project root. Any scripts you add to these directories will be executed before 26 | and after the commands corresponding to the directory name. Useful for 27 | integrating your own build systems or integrating with version control systems. 28 | 29 | __Remember__: Make your scripts executable. 30 | 31 | ## Hook Directories 32 | The following subdirectories will be used for hooks: 33 | 34 | after_build/ 35 | after_compile/ 36 | after_docs/ 37 | after_emulate/ 38 | after_platform_add/ 39 | after_platform_rm/ 40 | after_platform_ls/ 41 | after_plugin_add/ 42 | after_plugin_ls/ 43 | after_plugin_rm/ 44 | after_plugin_search/ 45 | after_prepare/ 46 | after_run/ 47 | after_serve/ 48 | before_build/ 49 | before_compile/ 50 | before_docs/ 51 | before_emulate/ 52 | before_platform_add/ 53 | before_platform_rm/ 54 | before_platform_ls/ 55 | before_plugin_add/ 56 | before_plugin_ls/ 57 | before_plugin_rm/ 58 | before_plugin_search/ 59 | before_prepare/ 60 | before_run/ 61 | before_serve/ 62 | pre_package/ <-- Windows 8 and Windows Phone only. 63 | 64 | ## Script Interface 65 | 66 | All scripts are run from the project's root directory and have the root directory passes as the first argument. All other options are passed to the script using environment variables: 67 | 68 | * CORDOVA_VERSION - The version of the Cordova-CLI. 69 | * CORDOVA_PLATFORMS - Comma separated list of platforms that the command applies to (e.g.: android, ios). 70 | * CORDOVA_PLUGINS - Comma separated list of plugin IDs that the command applies to (e.g.: org.apache.cordova.file, org.apache.cordova.file-transfer) 71 | * CORDOVA_HOOK - Path to the hook that is being executed. 72 | * CORDOVA_CMDLINE - The exact command-line arguments passed to cordova (e.g.: cordova run ios --emulate) 73 | 74 | If a script returns a non-zero exit code, then the parent cordova command will be aborted. 75 | 76 | 77 | ## Writing hooks 78 | 79 | We highly recommend writting your hooks using Node.js so that they are 80 | cross-platform. Some good examples are shown here: 81 | 82 | [http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/](http://devgirl.org/2013/11/12/three-hooks-your-cordovaphonegap-project-needs/) 83 | 84 | -------------------------------------------------------------------------------- /hooks/after_prepare/010_add_platform_class.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Add Platform Class 4 | // v1.0 5 | // Automatically adds the platform class to the body tag 6 | // after the `prepare` command. By placing the platform CSS classes 7 | // directly in the HTML built for the platform, it speeds up 8 | // rendering the correct layout/style for the specific platform 9 | // instead of waiting for the JS to figure out the correct classes. 10 | 11 | var fs = require('fs'); 12 | var path = require('path'); 13 | 14 | var rootdir = process.argv[2]; 15 | 16 | function addPlatformBodyTag(indexPath, platform) { 17 | // add the platform class to the body tag 18 | try { 19 | var platformClass = 'platform-' + platform; 20 | var cordovaClass = 'platform-cordova platform-webview'; 21 | 22 | var html = fs.readFileSync(indexPath, 'utf8'); 23 | 24 | var bodyTag = findBodyTag(html); 25 | if(!bodyTag) return; // no opening body tag, something's wrong 26 | 27 | if(bodyTag.indexOf(platformClass) > -1) return; // already added 28 | 29 | var newBodyTag = bodyTag; 30 | 31 | var classAttr = findClassAttr(bodyTag); 32 | if(classAttr) { 33 | // body tag has existing class attribute, add the classname 34 | var endingQuote = classAttr.substring(classAttr.length-1); 35 | var newClassAttr = classAttr.substring(0, classAttr.length-1); 36 | newClassAttr += ' ' + platformClass + ' ' + cordovaClass + endingQuote; 37 | newBodyTag = bodyTag.replace(classAttr, newClassAttr); 38 | 39 | } else { 40 | // add class attribute to the body tag 41 | newBodyTag = bodyTag.replace('>', ' class="' + platformClass + ' ' + cordovaClass + '">'); 42 | } 43 | 44 | html = html.replace(bodyTag, newBodyTag); 45 | 46 | fs.writeFileSync(indexPath, html, 'utf8'); 47 | 48 | process.stdout.write('add to body class: ' + platformClass + '\n'); 49 | } catch(e) { 50 | process.stdout.write(e); 51 | } 52 | } 53 | 54 | function findBodyTag(html) { 55 | // get the body tag 56 | try{ 57 | return html.match(/])(.*?)>/gi)[0]; 58 | }catch(e){} 59 | } 60 | 61 | function findClassAttr(bodyTag) { 62 | // get the body tag's class attribute 63 | try{ 64 | return bodyTag.match(/ class=["|'](.*?)["|']/gi)[0]; 65 | }catch(e){} 66 | } 67 | 68 | if (rootdir) { 69 | 70 | // go through each of the platform directories that have been prepared 71 | var platforms = (process.env.CORDOVA_PLATFORMS ? process.env.CORDOVA_PLATFORMS.split(',') : []); 72 | 73 | for(var x=0; x 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /www/js/app.js: -------------------------------------------------------------------------------- 1 | // Ionic Starter App 2 | 3 | // angular.module is a global place for creating, registering and retrieving Angular modules 4 | // 'starter' is the name of this angular module example (also set in a attribute in index.html) 5 | // the 2nd parameter is an array of 'requires' 6 | // 'starter.controllers' is found in controllers.js 7 | angular.module('starter', ['ionic', 'firebase','starter.controllers','starter.services']) 8 | 9 | .run(function($ionicPlatform, $rootScope, $state, AuthService) { 10 | $ionicPlatform.ready(function(){ 11 | AuthService.userIsLoggedIn().then(function(response) 12 | { 13 | if(response === true) 14 | { 15 | $state.go('app.user'); 16 | } 17 | else 18 | { 19 | $state.go('auth.login'); 20 | } 21 | }); 22 | 23 | // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard 24 | // for form inputs) 25 | if (window.cordova && window.cordova.plugins.Keyboard) { 26 | cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); 27 | cordova.plugins.Keyboard.disableScroll(true); 28 | } 29 | if (window.StatusBar) { 30 | // org.apache.cordova.statusbar required 31 | StatusBar.styleDefault(); 32 | } 33 | }); 34 | 35 | // UI Router Authentication Check 36 | $rootScope.$on("$stateChangeStart", function(event, toState, toParams, fromState, fromParams){ 37 | if (toState.data.authenticate) 38 | { 39 | AuthService.userIsLoggedIn().then(function(response) 40 | { 41 | if(response === false) 42 | { 43 | event.preventDefault(); 44 | $state.go('auth.login'); 45 | } 46 | }); 47 | } 48 | }); 49 | }) 50 | 51 | .config(function($stateProvider, $urlRouterProvider) { 52 | 53 | // Ionic uses AngularUI Router which uses the concept of states 54 | // Learn more here: https://github.com/angular-ui/ui-router 55 | // Set up the various states which the app can be in. 56 | // Each state's controller can be found in controllers.js 57 | $stateProvider 58 | 59 | // setup an abstract state for the auth section 60 | .state('auth', { 61 | url: '/auth', 62 | abstract: true 63 | }) 64 | 65 | .state('auth.login', { 66 | url: '/login', 67 | views: { 68 | 'main-view@': { 69 | templateUrl: 'templates/auth/login.html', 70 | controller: 'LogInCtrl' 71 | } 72 | }, 73 | data: { 74 | authenticate: false 75 | } 76 | }) 77 | 78 | .state('auth.signup', { 79 | url: '/signup', 80 | views: { 81 | 'main-view@': { 82 | templateUrl: 'templates/auth/signup.html', 83 | controller: 'SignUpCtrl' 84 | } 85 | }, 86 | data: { 87 | authenticate: false 88 | } 89 | }) 90 | 91 | .state('app', { 92 | url: '/app', 93 | abstract: true 94 | }) 95 | 96 | .state('app.user', { 97 | url: '/user', 98 | views: { 99 | 'main-view@': { 100 | templateUrl: 'templates/app/user.html', 101 | controller: 'UserCtrl' 102 | } 103 | }, 104 | data: { 105 | authenticate: true 106 | } 107 | }) 108 | 109 | ; 110 | 111 | // if none of the above states are matched, use this as the fallback 112 | $urlRouterProvider.otherwise('/app/user'); 113 | }); 114 | -------------------------------------------------------------------------------- /www/js/controllers.js: -------------------------------------------------------------------------------- 1 | angular.module('starter.controllers', []) 2 | 3 | .controller('LogInCtrl', function($scope, $state, AuthService, $ionicLoading) { 4 | $scope.login = function(user){ 5 | $ionicLoading.show({ 6 | template: 'Logging in ...' 7 | }); 8 | 9 | AuthService.doLogin(user) 10 | .then(function(user){ 11 | // success 12 | $state.go('app.user'); 13 | $ionicLoading.hide(); 14 | },function(err){ 15 | // error 16 | $scope.errors = err; 17 | $ionicLoading.hide(); 18 | }); 19 | }; 20 | 21 | $scope.facebookLogin = function(){ 22 | $ionicLoading.show({ 23 | template: 'Logging in with Facebook ...' 24 | }); 25 | 26 | AuthService.doFacebookLogin() 27 | .then(function(user){ 28 | // success 29 | $state.go('app.user'); 30 | $ionicLoading.hide(); 31 | },function(err){ 32 | // error 33 | $scope.errors = err; 34 | $ionicLoading.hide(); 35 | }); 36 | }; 37 | }) 38 | 39 | .controller('SignUpCtrl', function($scope, $state, AuthService, $ionicLoading) { 40 | $scope.signup = function(user){ 41 | $ionicLoading.show({ 42 | template: 'Signing up ...' 43 | }); 44 | 45 | AuthService.doSignup(user) 46 | .then(function(user){ 47 | // success 48 | $state.go('app.user'); 49 | $ionicLoading.hide(); 50 | },function(err){ 51 | // error 52 | $scope.errors = err; 53 | $ionicLoading.hide(); 54 | }); 55 | }; 56 | }) 57 | 58 | .controller('UserCtrl', function($scope, $state, AuthService){ 59 | $scope.current_user = {}; 60 | 61 | var current_user = AuthService.getUser(); 62 | 63 | if(current_user && current_user.provider == "facebook"){ 64 | $scope.current_user.email = current_user.facebook.displayName; 65 | $scope.current_user.image = current_user.facebook.profileImageURL; 66 | } else { 67 | $scope.current_user.email = current_user.password.email; 68 | $scope.current_user.image = current_user.password.profileImageURL; 69 | } 70 | 71 | $scope.logout = function(){ 72 | AuthService.doLogout(); 73 | 74 | $state.go('auth.login'); 75 | }; 76 | }) 77 | 78 | ; 79 | -------------------------------------------------------------------------------- /www/js/services.js: -------------------------------------------------------------------------------- 1 | angular.module('starter.services', []) 2 | 3 | .service('AuthService', function($q){ 4 | var _firebase = new Firebase("https://logfirebase.firebaseio.com/"); 5 | 6 | this.userIsLoggedIn = function(){ 7 | var deferred = $q.defer(), 8 | authService = this, 9 | isLoggedIn = (authService.getUser() !== null); 10 | 11 | deferred.resolve(isLoggedIn); 12 | 13 | return deferred.promise; 14 | }; 15 | 16 | this.getUser = function(){ 17 | return _firebase.getAuth(); 18 | }; 19 | 20 | this.doLogin = function(user){ 21 | var deferred = $q.defer(); 22 | 23 | _firebase.authWithPassword({ 24 | email : user.email, 25 | password : user.password 26 | }, function(errors, data) { 27 | if (errors) { 28 | var errors_list = [], 29 | error = { 30 | code: errors.code, 31 | msg: errors.message 32 | }; 33 | errors_list.push(error); 34 | deferred.reject(errors_list); 35 | } else { 36 | deferred.resolve(data); 37 | } 38 | }); 39 | 40 | return deferred.promise; 41 | }; 42 | 43 | this.doFacebookLogin = function(){ 44 | var deferred = $q.defer(); 45 | 46 | _firebase.authWithOAuthPopup("facebook", function(errors, data) { 47 | if (errors) { 48 | var errors_list = [], 49 | error = { 50 | code: errors.code, 51 | msg: errors.message 52 | }; 53 | errors_list.push(error); 54 | deferred.reject(errors_list); 55 | } else { 56 | deferred.resolve(data); 57 | } 58 | }); 59 | 60 | return deferred.promise; 61 | }; 62 | 63 | this.doSignup = function(user){ 64 | var deferred = $q.defer(), 65 | authService = this; 66 | 67 | _firebase.createUser({ 68 | email : user.email, 69 | password : user.password, 70 | }, function(errors, data) { 71 | if (errors) { 72 | var errors_list = [], 73 | error = { 74 | code: errors.code, 75 | msg: errors.message 76 | }; 77 | errors_list.push(error); 78 | deferred.reject(errors_list); 79 | } else { 80 | // After signup we should automatically login the user 81 | authService.doLogin(user) 82 | .then(function(data){ 83 | // success 84 | deferred.resolve(data); 85 | },function(err){ 86 | // error 87 | deferred.reject(err); 88 | }); 89 | } 90 | }); 91 | 92 | return deferred.promise; 93 | }; 94 | 95 | this.doLogout = function(){ 96 | _firebase.unauth(); 97 | }; 98 | }) 99 | 100 | ; 101 | -------------------------------------------------------------------------------- /www/templates/app/user.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

User

4 |
5 | 6 |
7 |
8 | 9 |

Welcome

10 |

{{ current_user.email }}

11 |
12 |
13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /www/templates/auth/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Log In

4 |
5 | 6 | 7 |
8 |
9 |

OR

10 |
11 |
12 |
13 |
14 | 17 | 20 |
21 | 22 |
23 | 26 |
27 |

[{{error.code}}] {{error.msg}}

28 |
29 |
30 |
31 | -------------------------------------------------------------------------------- /www/templates/auth/signup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Sign Up

4 |
5 | 6 |
7 |
8 | 11 | 14 |
15 | 16 |
17 | 20 |
21 |

[{{error.code}}] {{error.msg}}

22 |
23 |
24 |
25 | --------------------------------------------------------------------------------