Статьи

Аутентификация Firebase и Angular с Auth0: Часть 1

Эта статья была первоначально опубликована в блоге Auth0.com и публикуется здесь с разрешения.

В этой серии из двух частей мы узнаем, как создать приложение, которое защищает внутренний узел Node и внешний интерфейс Angular с аутентификацией Auth0 . Наш сервер и приложение также будут аутентифицировать базу данных Firebase Cloud Firestore с помощью пользовательских токенов, чтобы пользователи могли безопасно оставлять комментарии в реальном времени после входа в систему с помощью Auth0.

Код приложения Angular можно найти в репозитории angit-firebase GitHub, а API-интерфейс Node — в репозитории firebase-auth0-nodeserver .

Аутентификация Firebase и Angular с Auth0: Часть 1

Часть 1 нашего урока будет охватывать:

  1. Firebase и Auth0
  2. Что мы будем строить
  3. Угловой CLI
  4. Клиент Auth0 и API
  5. Проект Firebase с сервисной учетной записью
  6. Node API
  7. Настроить угловое приложение
  8. Архитектура угловых приложений
  9. Реализация общих модулей
  10. Реализация модулей маршрутизации и отложенной загрузки
  11. Загрузка и ошибка компонентов
  12. Логика аутентификации
  13. Основная логика
  14. Следующие шаги

Firebase и Auth0

Firebase — это платформа для разработки мобильных приложений и веб-приложений. Firebase была приобретена Google в 2014 году и продолжает развиваться под эгидой Google. Firebase предоставляет базы данных NoSQL ( RTDB или Realtime Database и Cloud Firestore, в бета-версии на момент написания ), размещенные в облаке и подключенные через веб-сокеты, чтобы предоставлять приложениям возможности в реальном времени.

Auth0 — это облачная платформа, которая обеспечивает аутентификацию и авторизацию как сервис. Как поставщик аутентификации, Auth0 позволяет разработчикам легко внедрять и настраивать безопасность входа и авторизации для своих приложений.

Выбор Auth0 + Firebase Authentication

Если вы уже знакомы с предложениями Firebase, вы можете спросить: почему мы должны реализовывать Auth0 с пользовательскими токенами в Firebase, а не придерживаться встроенной аутентификации Firebase ?

Во-первых, здесь есть важное различие. Использование Auth0 для защиты Firebase не означает, что вы не используете Firebase auth. Firebase имеет собственный подход к аутентификации, который позволяет разработчикам интегрировать предпочтительное решение для идентификации с аутентификацией Firebase. Этот подход позволяет разработчикам реализовать аутентификацию Firebase, чтобы она беспрепятственно работала с проприетарными системами или другими поставщиками аутентификации.

Есть много потенциальных причин, по которым мы могли бы захотеть интегрировать Auth0 с аутентификацией Firebase. В качестве альтернативы, существуют сценарии, в которых может быть достаточным использование базовой аутентификации Firebase. Давайте исследуем.

Вы можете использовать встроенную аутентификацию Firebase самостоятельно, если вы:

  • Только хотите аутентифицировать Firebase RTDB или Firestore и не нужно аутентифицировать дополнительные серверные части
  • Требуется лишь небольшое количество параметров входа в систему и не требуются корпоративные поставщики удостоверений, интеграция с собственными базами данных пользовательских хранилищ и т. Д.
  • Не требует обширного управления пользователями, обогащения профилей и т. Д. И удобен для управления пользователями строго через API
  • Не нужно настраивать потоки аутентификации
  • Не нужно придерживаться правил соответствия, касающихся хранения пользовательских данных.

Вы должны рассмотреть Auth0 с пользовательским токеном Firebase, если вы:

По сути, базовых провайдеров аутентификации Firebase должно быть достаточно, если у вас очень простое приложение с необходимыми аутентификационными требованиями и вы используете только базы данных Firebase. Однако, если вам нужно больше, Firebase предлагает отличный способ использовать свои услуги с другими решениями аутентификации . Это гораздо более реалистичный сценарий, с которым столкнутся многие разработчики, поэтому мы подробно рассмотрим его здесь.

Что мы будем строить

Мы собираемся создать API-интерфейс Node.js, защищенный с помощью Auth0, который обрабатывает пользовательские токены Firebase, а также возвращает данные о десяти различных породах собак.

Мы также создадим приложение Angular, называемое «Popular Dogs», которое отображает информацию о десяти самых популярных собаках в 2016 году, ранжированных по популярности среди публики Американским Кеннел Клубом (AKC). Наше приложение будет защищено Auth0, вызовет API-интерфейс Node для извлечения данных о собаках и вызовет API-интерфейс для получения токенов Firebase, которые позволят пользователям добавлять и удалять комментарии в реальном времени с помощью Cloud Firestore. Приложение будет использовать общие модули, а также осуществлять ленивую загрузку.

Приложение Angular Firebase с пользовательскими токенами Auth0

Для реализации приложения вам понадобится следующее:

  • Угловой CLI
  • Бесплатная учетная запись Auth0 с клиентом и настроенным API
  • Бесплатный проект Firebase с сервисной учетной записью

Давайте начнем!

Угловой CLI

Убедитесь, что на вашем локальном компьютере установлен Node.js с NPM . Выполните следующую команду, чтобы установить Angular CLI глобально:

$ npm install -g @angular/cli@latest 

Мы создадим наше приложение Angular и почти всю его архитектуру с помощью CLI.

Клиент Auth0 и API

Вам понадобится учетная запись Auth0 для управления аутентификацией. Вы можете подписаться на бесплатный аккаунт здесь .

Auth0 экран входа

Затем настройте клиентское приложение Auth0 и API, чтобы Auth0 мог взаимодействовать с приложением Angular и Node API.

Настройка клиента Auth0

  1. Перейдите на панель инструментов Auth0 и нажмите кнопку « Создать нового клиента» .
  2. Назовите свое новое приложение (например, Angular Firebase ) и выберите « Одностраничные веб-приложения» .
  3. В настройках вашего нового клиентского приложения Auth0 добавьте http://localhost:4200/callback к URL-адресам Allowed Callback .
  4. Включите переключатель Использовать Auth0 вместо IdP для единого входа .
  5. В нижней части раздела «Настройки» нажмите «Показать дополнительные настройки». Выберите вкладку OAuth и убедитесь, что для алгоритма подписи JsonWebToken установлено значение «RS256».
  6. Если вы хотите, вы можете установить некоторые социальные связи . Затем вы можете включить их для своего приложения в настройках клиента на вкладке Подключения . Пример, показанный на снимке экрана выше, использует базу данных имени пользователя / пароля, Facebook, Google и Twitter.

Примечание. Для производства убедитесь, что вы настроили свои собственные социальные ключи и не оставляете социальные связи настроенными на использование ключей разработчика Auth0.

Настройте API Auth0

  1. Зайдите в API на панели инструментов Auth0 и нажмите кнопку «Создать API». Введите имя для API, например, Firebase Dogs API . Установите идентификатор для URL-адреса конечной точки API. В этом руководстве наш идентификатор API — http://localhost:1337/ . Алгоритм подписи должен быть «RS256».
  2. Вы можете обратиться к примеру Node.js на вкладке Быстрый старт в настройках вашего нового API. На следующих шагах мы реализуем наш Node API таким образом, используя Express , express-jwt и jwks-rsa .

Теперь мы готовы реализовать аутентификацию Auth0 как на нашем клиенте Angular, так и на внутреннем API Node.

Проект Firebase с сервисной учетной записью

Далее вам понадобится бесплатный проект Firebase .

Создать проект Firebase

  1. Перейдите в консоль Firebase и войдите в свою учетную запись Google.
  2. Нажмите на Добавить проект .
  3. В Angular Firebase Auth0 диалоговом окне Angular Firebase Auth0 имя вашему проекту (например, Angular Firebase Auth0 ). Идентификатор проекта будет создан на основе выбранного вами имени. Затем вы можете выбрать свою страну / регион.
  4. Нажмите кнопку « Создать проект» .

Создать ключ SDK администратора

Чтобы копировать пользовательские токены Firebase , вам понадобится доступ к Firebase Admin SDK . Чтобы получить доступ, вы должны создать служебную учетную запись в вашем новом проекте Firebase.

Нажмите на значок шестерни рядом с вашим обзором проекта на боковой панели консоли Firebase и выберите « Настройки проекта» в появившемся меню:

Настройка проекта Firebase

В представлении настроек перейдите на вкладку « Учетные записи служб ». Появится пользовательский интерфейс Firebase Admin SDK с фрагментом кода конфигурации. Node.js выбран по умолчанию. Это технология, которую мы хотим, и мы реализуем ее в нашем Node API. Нажмите кнопку « Создать новый закрытый ключ» .

Появится диалоговое окно, предупреждающее вас о конфиденциальности вашего личного ключа. Мы позаботимся о том, чтобы этот ключ не был включен в публичный репозиторий. Нажмите кнопку « Создать ключ» , чтобы загрузить ключ в виде файла .json . Мы добавим этот файл в наш Node API в ближайшее время.

Node API

Завершенный API-интерфейс Node.js для этого руководства можно найти в репозитории GitHub firebase-auth0-nodeserver . Давайте узнаем, как построить этот API.

Файловая структура Node API

Мы хотим установить следующую структуру файлов:

 firebase-auth0-nodeserver/ |--firebase/ |--.gitignore |--<your-firebase-admin-sdk-key>.json |--.gitignore |--config.js |--dogs.json |--package.json |--routes.js |--server.js 

Вы можете сгенерировать необходимые папки и файлы с помощью командной строки следующим образом:

 $ mkdir firebase-auth0-nodeserver $ cd firebase-auth0-nodeserver $ mkdir firebase $ touch firebase/.gitignore $ touch .gitignore $ touch config.js $ touch dogs.json $ touch package.json $ touch routes.js $ touch server.js 

Ключ SDK администратора Firebase и игнорирование Git

Теперь переместите файл ключа Firebase Admin SDK .json который вы загрузили ранее, в папку Firebase. Мы позаботимся о том, чтобы папка была firebase/.gitignore , но ее содержимое никогда не firebase/.gitignore с помощью firebase/.gitignore например так:

 # firebase/.gitignore * */ !.gitignore 

Эта конфигурация .gitignore гарантирует, что Git будет игнорировать любые файлы и папки в каталоге .gitignore кроме .gitignore файла .gitignore . Это позволяет нам зафиксировать (по существу) пустую папку. Наш SDK-ключ .json Firebase может .json в этой папке, и нам не нужно беспокоиться о том, чтобы изменить его по имени файла .

Примечание. Это особенно полезно, если мы разобрали проект на нескольких машинах и сгенерировали разные ключи (с разными именами файлов).

Далее давайте добавим код для корневого каталога .gitignore :

 # .gitignore config.js node_modules 

Собаки JSON Data

Далее мы добавим данные для десяти пород собак. Для краткости вы можете просто скопировать и вставить эти данные в свой файл dogs.json .

зависимости

Давайте добавим наш файл package.json так:

 { "name": "firebase-auth0-nodeserver", "version": "0.1.0", "description": "Node.js server that authenticates with an Auth0 access token and returns a Firebase auth token.", "repository": "https://github.com/auth0-blog/firebase-auth0-nodeserver", "main": "server.js", "scripts": { "start": "node server" }, "author": "Auth0", "license": "MIT", "dependencies": {}, "devDependencies": {} } 

Мы установим зависимости с помощью командной строки, и последние версии будут автоматически сохранены в файл package.json :

 $ npm install --save body-parser cors express express-jwt jwks-rsa firebase-admin 

Нам понадобятся body-parser , cors и express для обслуживания наших конечных точек API. Аутентификация будет опираться на express-jwt и jwks-rsa , в то время как чеканка токенов Firebase реализована с помощью firebase-admin SDK (к которому мы будем иметь доступ, используя сгенерированный нами ключ).

конфигурация

В файле config.js добавьте следующий код и замените значения заполнителей своими собственными настройками:

 // config.js module.exports = { AUTH0_DOMAIN: '<Auth0 Domain>', // eg, you.auth0.com AUTH0_API_AUDIENCE: '<Auth0 API Audience>', // eg, http://localhost:1337/ FIREBASE_KEY: './firebase/<Firebase JSON>', // eg, your-project-firebase-adminsdk-xxxxx-xxxxxxxxxx.json FIREBASE_DB: '<Firebase Database URL>' // eg, https://your-project.firebaseio.com }; 

сервер

Имея наши данные, конфигурацию и зависимости, мы теперь можем реализовать наш сервер Node. Откройте файл server.js и добавьте:

 // server.js // Modules const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); // App const app = express(); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cors()); // Set port const port = process.env.PORT || '1337'; app.set('port', port); // Routes require('./routes')(app); // Server app.listen(port, () => console.log(`Server running on localhost:${port}`)); 

Это запустит наш Node-сервер с Express по адресу http://localhost:1337/ .

Примечание: обратите внимание, что это идентификатор API, который мы установили в Auth0.

API-маршруты

Далее откройте файл routes.js Здесь мы определим наши конечные точки API, защитим их и создадим собственные токены Firebase. Добавьте следующий код:

 // routes.js // Dependencies const jwt = require('express-jwt'); const jwks = require('jwks-rsa'); const firebaseAdmin = require('firebase-admin'); // Config const config = require('./config'); module.exports = function(app) { // Auth0 athentication middleware const jwtCheck = jwt({ secret: jwks.expressJwtSecret({ cache: true, rateLimit: true, jwksRequestsPerMinute: 5, jwksUri: `https://${config.AUTH0_DOMAIN}/.well-known/jwks.json` }), audience: config.AUTH0_API_AUDIENCE, issuer: `https://${config.AUTH0_DOMAIN}/`, algorithm: 'RS256' }); // Initialize Firebase Admin with service account const serviceAccount = require(config.FIREBASE_KEY); firebaseAdmin.initializeApp({ credential: firebaseAdmin.credential.cert(serviceAccount), databaseURL: config.FIREBASE_DB }); // GET object containing Firebase custom token app.get('/auth/firebase', jwtCheck, (req, res) => { // Create UID from authenticated Auth0 user const uid = req.user.sub; // Mint token using Firebase Admin SDK firebaseAdmin.auth().createCustomToken(uid) .then(customToken => // Response must be an object or Firebase errors res.json({firebaseToken: customToken}) ) .catch(err => res.status(500).send({ message: 'Something went wrong acquiring a Firebase token.', error: err }) ); }); // Set up dogs JSON data for API const dogs = require('./dogs.json'); const getDogsBasic = () => { const dogsBasicArr = dogs.map(dog => { return { rank: dog.rank, breed: dog.breed, image: dog.image } }); return dogsBasicArr; } // GET dogs (public) app.get('/api/dogs', (req, res) => { res.send(getDogsBasic()); }); // GET dog details by rank (private) app.get('/api/dog/:rank', jwtCheck, (req, res) => { const rank = req.params.rank * 1; const thisDog = dogs.find(dog => dog.rank === rank); res.send(thisDog); }); }; 

На высоком уровне наш файл маршрутов делает следующее:

  • Настраивает проверку подлинности, чтобы гарантировать, что только зарегистрированные пользователи могут получать доступ к маршрутам с jwtCheck промежуточного программного обеспечения jwtCheck
  • Инициализирует Firebase Admin SDK с помощью закрытого ключа, созданного из учетной записи службы проекта Firebase
  • Обеспечивает безопасную конечную точку GET которая возвращает пользовательский токен Firebase
  • Предоставляет общедоступную конечную точку GET *, которая возвращает краткую версию данных о собаках
  • Обеспечивает безопасную конечную точку GET *, которая возвращает подробные данные конкретной собаки, запрошенные по рангу.

* Конечные точки используют варианты одного и того же базового набора данных для моделирования более сложного API.

Вы можете прочитать комментарии к коду для более подробной информации.

Служить API

Вы можете использовать Node API, выполнив:

 $ node server 

После этого API будет доступен по адресу http: // localhost: 1337 .

Примечание. Если вы попытаетесь получить доступ к безопасным маршрутам в браузере, вы должны получить 401 Unauthorized ошибку.

Вот и все для нашего сервера! Оставьте API запущенным, чтобы он был доступен для приложения Angular, которое мы настроим позже.

Настроить угловое приложение

Теперь пришло время создать наше приложение Angular и настроить некоторые дополнительные зависимости.

Создать новое угловое приложение

Вы должны были уже установить Angular CLI ранее. Теперь мы можем использовать CLI для генерации нашего проекта и его архитектуры. Чтобы создать новое приложение, выберите содержащую его папку, а затем выполните следующую команду:

 $ ng new angular-firebase --routing --skip-tests 

Флаг --routing генерирует приложение с модулем маршрутизации, а --skip-tests генерирует корневой компонент без файла .spec.ts .

Примечание. Для краткости мы не будем рассматривать тестирование в этой статье. Если вы хотите узнать больше о тестировании в Angular, ознакомьтесь с заключением руководства для получения дополнительных ресурсов.

Установите интерфейсные зависимости

Теперь давайте установим наши клиентские зависимости:

 $ cd angular-firebase $ npm install --save auth0-js@latest firebase@latest angularfire2@latest 

Нам понадобится библиотека auth0-js для реализации аутентификации Auth0 в нашем приложении Angular. Нам также понадобится JS SDK для Firebase и библиотека angularfire2 Angular Firebase для реализации наших комментариев в реальном времени с помощью Firebase.

Добавить Bootstrap CSS

Чтобы упростить стилизацию, мы добавим ссылку CDN Bootstrap CSS на <head> нашего файла index.html следующим образом:

 <!-- src/index.html --> ... <head> ... <title>Top 10 Dogs</title> ... <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> </head> ... 

Подать угловое приложение

Вы можете обслуживать приложение Angular с помощью следующей команды:

 $ ng serve 

Приложение будет работать в браузере по адресу http: // localhost: 4200 .

Архитектура угловых приложений

Мы собираемся использовать Angular CLI для создания полной архитектуры нашего приложения. Таким образом, мы можем убедиться, что наши модули работают должным образом, прежде чем мы реализуем нашу логику и шаблоны.

Наше приложение будет использовать модульный подход с отложенной загрузкой . Пример приложения в этом руководстве небольшой, но мы хотим создать его в масштабируемом, реальном виде.

Корневой модуль

Корневой модуль уже был создан, когда приложение Angular было создано с помощью команды ng new . Корневой модуль находится в src/app/app.module.ts . Любые компоненты, которые мы генерируем в нашем приложении Angular без указания подкаталога другого модуля, будут автоматически импортированы и объявлены в нашем корневом модуле.

Давайте теперь сгенерируем компонент с помощью CLI:

 # create CallbackComponent: $ ng g component callback --is --it --flat --no-spec 

Эта команда состоит из следующего:

  • ng g component : генерирует файл компонента callback с:
  • --is встроенные стили
  • --it встроенный шаблон
  • --flat не содержит папку
  • --no-spec no .spec тестовый файл

Мы будем использовать компонент обратного вызова для обработки перенаправления после входа пользователя в наше приложение. Это очень простой компонент.

Примечание: g является ярлыком для generate . Мы также можем использовать c в качестве ярлыка для component , делая эту команду ng gc . Тем не менее, этот учебник не будет использовать ярлыки для типа сгенерированных файлов, для ясности.

Базовая модульная архитектура

Далее мы создадим CoreModule его компоненты и сервисы. Это общий модуль. В корне вашей папки проекта Angular выполните следующие команды консоли. Убедитесь, что вы сначала запустите команду ng g module core , например:

 # create Core module: $ ng g module core # create API service with no .spec file: $ ng g service core/api --no-spec # create HeaderComponent with inline styles, no .spec file, and export in module: $ ng g component core/header --is --no-spec --export=true # create LoadingComponent with inline styles, inline template, no folder, no .spec file, and export in module: $ ng g component core/loading --is --it --flat --no-spec --export=true # create ErrorComponent with inline styles, inline template, no folder, no .spec file, and export in module: $ ng g component core/error --is --it --flat --no-spec --export=true # create Dog type interface: $ ng g interface core/dog # create DogDetail type interface: $ ng g interface core/dog-detail 

Сначала создание модуля гарантирует, что компоненты, созданные в папке этого модуля, будут затем импортированы и объявлены автоматически в этом родительском модуле вместо корневого модуля приложения.

Примечание. Если вы хотите использовать компоненты общего модуля в другом модуле, необходимо export компоненты, а также объявить их. Мы можем сделать это автоматически с CLI, используя флаг --export=true .

Это базовая архитектура для общих базовых сервисов, компонентов и моделей, к которым нашему приложению потребуется доступ.

Auth Module Architecture

Далее мы создадим наш AuthModule . Выполните следующие команды CLI (снова, сначала убедитесь, что модуль сгенерирован):

 # create Auth module: $ ng g module auth # create AuthService with no .spec file: $ ng g service auth/auth --no-spec # create Auth route guard with no .spec file: $ ng g guard auth/auth --no-spec 

Наш модуль Auth обеспечивает сервис и защиту маршрута, необходимые для управления аутентификацией, но не имеет никаких компонентов. Это также общий модуль.

Модульная архитектура собак

Домашняя страница нашего приложения будет предоставлена DogsModule . Это будет список десяти самых популярных собак 2016 года по версии AKC. Используйте следующие команды CLI, чтобы сгенерировать структуру для этого лениво загруженного модуля страницы:

 # create Dogs module: $ ng g module dogs # create DogsComponent with inline styles and no .spec file: $ ng g component dogs/dogs --is --no-spec 

Собака модульная архитектура

Наше приложение также будет содержать подробные страницы для каждой собаки, указанной в компоненте «Собаки», чтобы пользователи могли больше узнать о каждой породе. Используйте следующие команды CLI, чтобы сгенерировать структуру для лениво загруженного DogModule :

 # create Dog module: $ ng g module dog # create DogComponent with inline styles and no .spec file: $ ng g component dog/dog --is --no-spec 

Архитектура модуля комментариев

Наконец, нам нужно реализовать архитектуру, необходимую для комментариев в реальном времени в Firebase. Используйте следующие команды CLI, чтобы сгенерировать структуру для CommentsModule :

 # create Comments module: $ ng g module comments # create Comment model class: $ ng g class comments/comment # create CommentsComponent with no .spec file: $ ng g component comments/comments --no-spec --export=true # create CommentFormComponent with inline styles and no .spec file: $ ng g component comments/comments/comment-form --is --no-spec 

Конфигурация среды

Давайте добавим информацию о нашей конфигурации для Auth0 и Firebase в наш внешний интерфейс Angular. Откройте файл environment.ts и добавьте:

 // src/environments/environment.ts const FB_PROJECT_ID = '<FIREBASE_PROJECT_ID>'; export const environment = { production: false, auth: { clientId: '<AUTH0_CLIENT_ID>', clientDomain: '<AUTH0_DOMAIN>', // eg, you.auth0.com audience: '<AUTH0_API_AUDIENCE>', // eg, http://localhost:1337/ redirect: 'http://localhost:4200/callback', scope: 'openid profile email' }, firebase: { apiKey: '<FIREBASE_API_KEY>', authDomain: `${FB_PROJECT_ID}.firebaseapp.com`, databaseURL: `https://${FB_PROJECT_ID}.firebaseio.com`, projectId: FB_PROJECT_ID, storageBucket: `${FB_PROJECT_ID}.appspot.com`, messagingSenderId: '<FIREBASE_MESSAGING_SENDER_ID>' }, apiRoot: '<API URL>' // eg, http://localhost:1337/ (DO include trailing slash) }; 

Замените местозаполнители в <angle brackets> соответствующими данными Auth0, Firebase и API.

Вы можете найти свою конфигурацию Auth0 на панели инструментов Auth0 в настройках клиента и API, которые вы создали для этого урока.

Вы можете найти свою конфигурацию Firebase в обзоре проекта консоли Firebase после нажатия на большую иконку с надписью Добавить Firebase в свое веб-приложение , как показано ниже:

Добавьте Firebase в свое веб-приложение

Добавить изображение загрузки

Последнее, что мы сделаем перед тем, как приступить к реализации функциональности в нашем приложении Angular, это добавим загрузочное изображение. Создайте следующую папку: src/assets/images .

Затем сохраните загрузочный SVG-образ в эту папку:

Загрузка SVG

Реализация общих модулей

Давайте настроим наши модули. Мы импортируем общие модули ( CoreModule и AuthModule ) в наш корневой AppModule .

Основной модуль

Сначала мы реализуем наш CoreModule . Откройте файл core.module.ts и обновите его до следующего кода:

 // src/app/core/core.module.ts import { NgModule, ModuleWithProviders } from '@angular/core'; import { CommonModule } from '@angular/common'; import { HttpClientModule } from '@angular/common/http'; import { FormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; import { Title } from '@angular/platform-browser'; import { DatePipe } from '@angular/common'; import { HeaderComponent } from './header/header.component'; import { ApiService } from './api.service'; import { LoadingComponent } from './loading.component'; import { ErrorComponent } from './error.component'; @NgModule({ imports: [ CommonModule, RouterModule, HttpClientModule, // AuthModule is a sibling and can use this without us exporting it FormsModule ], declarations: [ HeaderComponent, LoadingComponent, ErrorComponent ], exports: [ FormsModule, // Export FormsModule so CommentsModule can use it HeaderComponent, LoadingComponent, ErrorComponent ] }) export class CoreModule { static forRoot(): ModuleWithProviders { return { ngModule: CoreModule, providers: [ Title, DatePipe, ApiService ] }; } } 

Поскольку это общий модуль, мы импортируем другие модули, сервисы и компоненты, к которым нам потребуется доступ через наше приложение.

Примечание. CommonModule импортируется во все модули, которые не являются корневыми модулями.

В нашем массиве imports мы добавим любые модули, которые могут понадобиться службам или компонентам в CoreModule или которые должны быть доступны для других модулей в нашем приложении. CLI должен был автоматически добавить любые сгенерированные компоненты в массив declarations . Массив exports должен содержать любые модули или компоненты, которые мы хотим сделать доступными для других модулей.

Обратите внимание, что мы импортировали ModuleWithProviders из @angular/core . Используя этот модуль, мы можем создать метод forRoot() который можно вызывать при импорте в корневой app.module.ts при app.module.ts CoreModule . Таким образом, мы можем гарантировать, что любые сервисы, которые мы добавляем в массив providers возвращаемый методом forRoot() остаются в нашем приложении одиночными . Таким образом, мы можем избежать непреднамеренных множественных случаев, если другие модули в нашем приложении также должны импортировать CoreModule .

Auth Module

Далее давайте добавим немного кода в наш AuthModule в файле auth.module.ts :

 // src/app/auth/auth.module.ts import { NgModule, ModuleWithProviders } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AuthService } from './auth.service'; import { AuthGuard } from './auth.guard'; import { AngularFireAuthModule } from 'angularfire2/auth'; @NgModule({ imports: [ CommonModule, AngularFireAuthModule ] }) export class AuthModule { static forRoot(): ModuleWithProviders { return { ngModule: AuthModule, providers: [ AuthService, AuthGuard ] }; } } 

Мы импортируем ModuleWithProviders для реализации forRoot() как мы это делали с нашим CoreModule . Затем мы импортируем наш AuthService и AuthGuard . Нам также необходимо импортировать AngularFireAuthModule из angularfire2/auth чтобы мы могли защитить наши соединения Firebase в нашем AuthService . Служба и защита должны затем быть возвращены в массиве providers в forRoot() .

Модуль комментариев

Откройте файл comments.module.ts для реализации CommentsModule следующим образом:

 // src/app/comments/comments.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { environment } from './../../environments/environment'; import { AngularFireModule } from 'angularfire2'; import { AngularFirestoreModule } from 'angularfire2/firestore'; import { CommentsComponent } from './comments/comments.component'; import { CommentFormComponent } from './comments/comment-form/comment-form.component'; @NgModule({ imports: [ CommonModule, CoreModule, // Access FormsModule, Loading, and Error components AngularFireModule.initializeApp(environment.firebase), AngularFirestoreModule ], declarations: [ CommentsComponent, CommentFormComponent ], exports: [ CommentsComponent ] }) export class CommentsModule { } 

Нам нужно будет импортировать CoreModule чтобы мы могли использовать его экспортированные FormsModule , LoadingComponent и ErrorComponent . Нам также необходимо получить доступ к нашей конфигурации из файла environment.ts . Комментарии используют базу данных Firebase Cloud Firestore, поэтому давайте импортируем AngularFireModule и AngularFirestoreModule а также два наших компонента: CommentsComponent и CommentFormComponent .

Когда мы добавляем AngularFireModule в массив imports @ NgModule, мы вызываем его метод initializeApp() , передавая нашу конфигурацию Firebase. Оба наших компонента уже должны быть в массиве declarations , а компонент CommentsComponent уже должен быть добавлен в массив exports чтобы другие компоненты из других модулей могли использовать его.

Примечание: нам не нужно экспортировать CommentsFormComponent потому что это дочерний элемент CommentsComponent .

Модуль CommentsModule не предоставляет никаких сервисов, поэтому нет необходимости реализовывать метод forRoot() .

Модуль приложения

Теперь, когда наши CoreModule , AuthModule и CommentsModule были реализованы, нам нужно импортировать их в наш корневой модуль, AppModule расположенный в файле app.module.ts :

 // src/app/app.module.ts import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppRoutingModule } from './app-routing.module'; import { CoreModule } from './core/core.module'; import { AuthModule } from './auth/auth.module'; import { CommentsModule } from './comments/comments.module'; import { AppComponent } from './app.component'; import { CallbackComponent } from './callback.component'; @NgModule({ declarations: [ AppComponent, CallbackComponent ], imports: [ BrowserModule, AppRoutingModule, CoreModule.forRoot(), AuthModule.forRoot(), CommentsModule ], bootstrap: [AppComponent] }) export class AppModule { } 

AppComponent и CallbackComponent уже были автоматически добавлены CLI. Когда мы добавим наши CoreModule и AuthModule в массив AuthModule , мы вызовем метод forRoot() чтобы убедиться, что для их сервисов не создаются дополнительные экземпляры. Модуль CommentsModule не предоставляет никаких услуг, так что это не относится к этому модулю.

Реализация модулей маршрутизации и отложенной загрузки

У нас есть два модуля, которые требуют маршрутизации: DogsModule для основного списка собак и DogModule , который содержит компонент, показывающий страницу с подробностями о породе собак.

Маршрутизация приложений

Сначала давайте реализуем маршрутизацию нашего приложения. Откройте файл app-routing.module.ts и добавьте этот код:

 // src/app/app-routing.module.ts import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; import { CallbackComponent } from './callback.component'; import { AuthGuard } from './auth/auth.guard'; const routes: Routes = [ { path: '', loadChildren: './dogs/dogs.module#DogsModule', pathMatch: 'full' }, { path: 'dog', loadChildren: './dog/dog.module#DogModule', canActivate: [ AuthGuard ] }, { path: 'callback', component: CallbackComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } 

Мы импортируем наш CallbackComponent и AuthGuard . Остальные маршруты будут строковыми ссылками на модули, а не на импортированные компоненты, использующие свойство loadChildren .

Мы установим путь по умолчанию для загрузки дочерних маршрутов из DogsModule и путь 'dog' для загрузки дочерних маршрутов из DogModule . Путь 'dog' также должен быть защищен AuthGuard , который мы объявляем с canActivate свойства canActivate . Это может содержать массив охранников маршрута, если нам потребуется более одного. Наконец, маршрут 'callback' должен просто указывать на CallbackComponent .

Модуль Собаки

Давайте добавим немного кода в файл dogs.module.ts :

 // src/app/dogs/dogs.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; import { CoreModule } from '../core/core.module'; import { CommentsModule } from '../comments/comments.module'; import { DogsComponent } from './dogs/dogs.component'; const DOGS_ROUTES: Routes = [ { path: '', component: DogsComponent } ]; @NgModule({ imports: [ CommonModule, CoreModule, RouterModule.forChild(DOGS_ROUTES), CommentsModule ], declarations: [ DogsComponent ] }) export class DogsModule { } 

Мы импортируем Routes и RouterModule в дополнение к нашим CoreModule и CommentsModule (комментарии появятся на главной странице списка собак).

Этот модуль имеет дочерний маршрут, поэтому мы создадим константу, которая содержит массив для хранения нашего объекта маршрута. Единственный дочерний маршрут, который нам понадобится, наследует путь '' из app-routing.module.ts , поэтому его путь также должен быть '' . Он загрузит DogsComponent . В нашем массиве DOGS_ROUTES мы передадим нашу константу RouterModule в RouterModule forChild() .

Собака Модуль

DogModule работает аналогично DogsModule выше. Откройте dog.module.ts и добавьте следующее:

 // src/app/dog/dog.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; import { CoreModule } from '../core/core.module'; import { DogComponent } from './dog/dog.component'; const DOG_ROUTES: Routes = [ { path: ':rank', component: DogComponent } ]; @NgModule({ imports: [ CommonModule, CoreModule, RouterModule.forChild(DOG_ROUTES) ], declarations: [ DogComponent ] }) export class DogModule { } 

Одно из различий между этим модулем и модулем DogsModule состоит в том, что наш DOG_ROUTES имеет путь :rank . Таким образом, маршрут для каждой конкретной информации о собаке передается в виде сегмента URL, соответствующего рангу собаки в нашем списке десяти лучших пород собак, например:

 http://localhost:4200/dog/3 

Другое отличие состоит в том, что мы не будем импортировать модуль CommentsModule . Тем не менее, мы можем добавить комментарии к деталям собаки в будущем, если пожелаем.

Архитектура и маршрутизация нашего приложения завершены! Приложение должно успешно скомпилироваться и отобразиться в браузере, при этом ленивая загрузка работает правильно, чтобы загрузить общий код и код для определенного запрошенного маршрута.

Теперь мы готовы реализовать логику нашего приложения.

Загрузка и ошибка компонентов

Компоненты загрузки и ошибок являются основными, основными элементами пользовательского интерфейса, которые могут использоваться в разных местах нашего приложения. Давайте настроим их сейчас.

Загрузка компонента

LoadingComponent должен просто показать загрузочное изображение. (Вспомните, что мы уже сохранили один, когда настраивали архитектуру нашего приложения.) Однако оно должно быть способным отображать изображение большим и центрированным или маленьким и встроенным.

Откройте файл loading.component.ts и добавьте:

 // src/app/core/loading.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-loading', template: ` <div [ngClass]="{'inline': inline, 'text-center': !inline, 'py-2': !inline }"> <img src="/assets/images/loading.svg"> </div> `, styles: [` .inline { display: inline-block; } img { height: 80px; width: 80px; } .inline img { height: 24px; width: 24px; } `] }) export class LoadingComponent { @Input() inline: boolean; } 

Используя декоратор @Input() , мы можем передавать информацию в компонент от его родителя, сообщая ему, должны ли мы отображать компонент встроенным или нет. Мы будем использовать директиву NgClass ( [ngClass] ) в нашем шаблоне, чтобы условно добавить соответствующие стили для отображения, которое мы хотим. Отображение этого компонента в другом шаблоне будет выглядеть так:

 <!-- Large, full width, centered: --> <app-loading></app-loading> <!-- Inline: --> <app-loading inline="true"></app-loading> 

Компонент ошибки

Далее давайте быстро реализуем наш ErrorComponent . Этот компонент будет отображать простое сообщение об ошибке, если отображается. Откройте файл error.component.ts и добавьте:

 // src/app/core/error.component.ts import { Component } from '@angular/core'; @Component({ selector: 'app-error', template: ` <p class="alert alert-danger"> <strong>Error:</strong> There was an error retrieving data. </p> ` }) export class ErrorComponent { } 

Логика аутентификации

Теперь давайте реализуем код, необходимый для AuthModule функций нашего AuthModule . Нам понадобится служба аутентификации, чтобы построить заголовок в CoreModule , поэтому имеет смысл начать здесь. Мы уже установили необходимые зависимости (Auth0 и FirebaseAuth), так что давайте начнем.

Служба аутентификации

Прежде чем писать какой-либо код, мы определим, какие требования предъявляются к этой услуге. Мы должны:

  • Создайте метод login() , который позволит пользователям проходить аутентификацию с использованием Auth0
  • Если пользователю было предложено войти в систему, пытаясь получить доступ к защищенному маршруту, убедитесь, что после успешной аутентификации его можно перенаправить на этот маршрут.
  • Получить информацию о профиле пользователя и настроить его сеанс
  • Установите для приложения способ узнать, вошел ли пользователь в систему или нет
  • Запрос пользовательского токена Firebase из API с авторизацией из токена доступа Auth0
  • В случае успешного получения токена Firebase войдите в Firebase с помощью возвращенного токена и установите для приложения способ узнать, вошел ли пользователь в Firebase или нет
  • Срок действия пользовательских токенов, созданных Firebase, истекает через час, поэтому мы должны настроить способ автоматического продления срока действия токенов, срок действия которых истек
  • Создайте метод logout() для очистки сессии и выхода из Firebase.

Откройте файл auth.service.ts который мы создали ранее.

Для краткости обучения, пожалуйста, ознакомьтесь с полным кодом в файле auth.service.ts GitHub здесь .

Там много чего происходит, так что давайте пройдемся по шагам.

Сначала, как всегда, мы импортируем наши зависимости. Это включает в себя конфигурацию environment мы настроили ранее для предоставления наших настроек Auth0, Firebase и API, а также библиотек AngularFireAuth и HttpClient , AngularFireAuth , HttpClient для вызова API для получения пользовательского токена Firebase и необходимых импортов RxJS.

Вы можете обратиться к комментариям кода для описания частных и открытых членов нашего класса AuthService .

Далее идет наша функция конструктора, где мы сделаем Router , AngularFireAuth и HttpClient доступными для использования в нашем классе.

Метод login() выглядит следующим образом:

 login(redirect?: string) { // Set redirect after login const _redirect = redirect ? redirect : this.router.url; localStorage.setItem('auth_redirect', _redirect); // Auth0 authorize request this._auth0.authorize(); } 

Если в метод передается сегмент URL redirect , мы сохраним его в локальном хранилище. Если перенаправление не передается, мы просто сохраним текущий URL. Затем мы будем использовать экземпляр _auth0 мы создали в наших участниках, и вызовем метод authorh authorize() Auth0, чтобы перейти на страницу входа в Auth0, чтобы наш пользователь мог проходить аутентификацию.

Следующими тремя методами являются handleLoginCallback() , getUserInfo() и _setSession() :

 handleLoginCallback() { this.loading = true; // When Auth0 hash parsed, get profile this._auth0.parseHash((err, authResult) => { if (authResult && authResult.accessToken) { window.location.hash = ''; // Store access token this.accessToken = authResult.accessToken; // Get user info: set up session, get Firebase token this.getUserInfo(authResult); } else if (err) { this.router.navigate(['/']); this.loading = false; console.error(`Error authenticating: ${err.error}`); } }); } getUserInfo(authResult) { // Use access token to retrieve user's profile and set session this._auth0.client.userInfo(this.accessToken, (err, profile) => { if (profile) { this._setSession(authResult, profile); } else if (err) { console.warn(`Error retrieving profile: ${err.error}`); } }); } private _setSession(authResult, profile) { // Set tokens and expiration in localStorage const expiresAt = JSON.stringify((authResult.expiresIn * 1000) + Date.now()); localStorage.setItem('expires_at', expiresAt); this.userProfile = profile; // Session set; set loggedIn and loading this.loggedIn = true; this.loading = false; // Get Firebase token this._getFirebaseToken(); // Redirect to desired route this.router.navigateByUrl(localStorage.getItem('auth_redirect')); 

Эти методы достаточно понятны: они используют методы parseHash() и userInfo() для извлечения результатов аутентификации и получения профиля пользователя . Мы также установим свойства нашего сервиса для хранения необходимого состояния (например, загружается ли состояние аутентификации пользователя и, вошли ли они в систему или нет), будем обрабатывать ошибки, сохранять данные в нашем сервисе и локальном хранилище и перенаправлять в соответствующее маршрут.

Мы также собираемся использовать токен доступа результата аутентификации, чтобы авторизовать HTTP-запрос к нашему API для получения токена Firebase. Это делается с помощью _getFirebaseToken() и _firebaseAuth() :

  private _getFirebaseToken() { // Prompt for login if no access token if (!this.accessToken) { this.login(); } const getToken$ = () => { return this.http .get(`${environment.apiRoot}auth/firebase`, { headers: new HttpHeaders().set('Authorization', `Bearer ${this.accessToken}`) }); }; this.firebaseSub = getToken$().subscribe( res => this._firebaseAuth(res), err => console.error(`An error occurred fetching Firebase token: ${err.message}`) ); } private _firebaseAuth(tokenObj) { this.afAuth.auth.signInWithCustomToken(tokenObj.firebaseToken) .then(res => { this.loggedInFirebase = true; // Schedule token renewal this.scheduleFirebaseRenewal(); console.log('Successfully authenticated with Firebase!'); }) .catch(err => { const errorCode = err.code; const errorMessage = err.message; console.error(`${errorCode} Could not log into Firebase: ${errorMessage}`); this.loggedInFirebase = false; }); } 

Мы создадим getToken$ observable из запроса GET к конечной точке нашего API /auth/firebase и подпишемся на него. В случае успеха мы передадим возвращенный объект с пользовательским токеном Firebase _firebaseAuth() , который будет аутентифицироваться в Firebase с использованием метода signInWithCustomToken() Firebase .Этот метод возвращает обещание, и когда обещание выполнено, мы можем сообщить нашему приложению, что вход в Firebase прошел успешно. Мы также можем запланировать обновление токенов Firebase (мы скоро рассмотрим это). Мы исправим любые ошибки.

Срок действия нашего пользовательского токена Firebase истекает через 3600несколько секунд (1 час). Это только вдвое меньше срока действия токена доступа по умолчанию для Auth0 (который составляет 7200секунды или 2 часа). Чтобы наши пользователи не могли неожиданно потерять доступ к Firebase в середине сеанса, мы настроим автоматическое обновление токенов Firebase двумя способами: и .scheduleFirebaseRenewal()unscheduleFirebaseRenewal()

Примечание. Аналогичным способом вы можете реализовать автоматическое возобновление сеанса с Auth0, используя метод . Кроме того, вы можете использовать для восстановления не истекшего сеанса аутентификации в конструкторе, если пользователь уходит из приложения, а затем возвращается позже. Мы не будем освещать это в этом уроке, но это то, что вы должны попробовать сами!checkSession()checkSession()

 scheduleFirebaseRenewal() { // If user isn't authenticated, check for Firebase subscription // and unsubscribe, then return (don't schedule renewal) if (!this.loggedInFirebase) { if (this.firebaseSub) { this.firebaseSub.unsubscribe(); } return; } // Unsubscribe from previous expiration observable this.unscheduleFirebaseRenewal(); // Create and subscribe to expiration observable // Custom Firebase tokens minted by Firebase // expire after 3600 seconds (1 hour) const expiresAt = new Date().getTime() + (3600 * 1000); const expiresIn$ = Observable.of(expiresAt) .pipe( mergeMap( expires => { const now = Date.now(); // Use timer to track delay until expiration // to run the refresh at the proper time return Observable.timer(Math.max(1, expires - now)); } ) ); this.refreshFirebaseSub = expiresIn$ .subscribe( () => { console.log('Firebase token expired; fetching a new one'); this._getFirebaseToken(); } ); } unscheduleFirebaseRenewal() { if (this.refreshFirebaseSub) { this.refreshFirebaseSub.unsubscribe(); } } 

Чтобы запланировать автоматическое обновление токена, мы создадим наблюдаемый таймер, который отсчитывает время истечения токена. Мы можем подписаться на expiresIn$наблюдаемое и затем снова вызвать наш метод, чтобы получить новый токен. Метод autular angularfire2 возвращает обещание. Когда обещание разрешается, вызывается, что, в свою очередь, гарантирует, что токен будет продолжать обновляться, пока пользователь входит в наше приложение._getFirebaseToken()signInWithCustomToken()scheduleFirebaseRenewal()

Нам также нужно будет отменить подписку на обновление токенов, поэтому мы также создадим метод для этого.

Наконец, последние два метода в нашей службе аутентификации являются и :logout()tokenValid()

 logout() { // Ensure all auth items removed localStorage.removeItem('expires_at'); localStorage.removeItem('auth_redirect'); this.accessToken = undefined; this.userProfile = undefined; this.loggedIn = false; // Sign out of Firebase this.loggedInFirebase = false; this.afAuth.auth.signOut(); // Return to homepage this.router.navigate(['/']); } get tokenValid(): boolean { // Check if current time is past access token's expiration const expiresAt = JSON.parse(localStorage.getItem('expires_at')); return Date.now() < expiresAt; } 

The logout() method removes all session information from local storage and from our service, signs out of Firebase Auth, and redirects the user back to the homepage (the only public route in our app).

The tokenValid accessor method checks whether the Auth0 access token is expired or not by comparing its expiration to the current datetime. This can be useful for determining if the user needs a new access token; we won’t cover that in this tutorial, but you may want to explore Auth0 session renewal further on your own.

That’s it for our AuthService !

Компонент обратного вызова

Recall that we created a CallbackComponent in our root module. In addition, we set our environment ‘s Auth0 redirect to the callback component’s route. That means that when the user logs in with Auth0, they will return to our app at the /callback route with the authentication hash appended to the URI.

We created our AuthService with methods to handle authentication and set sessions, but currently these methods aren’t being called from anywhere. The callback component is the appropriate place for this code to execute.

Open the callback.component.ts file and add:

 // src/app/callback.component.ts import { Component, OnInit } from '@angular/core'; import { AuthService } from './auth/auth.service'; @Component({ selector: 'app-callback', template: ` <app-loading></app-loading> ` }) export class CallbackComponent implements OnInit { constructor(private auth: AuthService) { } ngOnInit() { this.auth.handleLoginCallback(); } } 

All our callback component needs to do is show the LoadingComponent while the AuthService ‘s handleAuth() method executes. The handleLoginCallback() method will parse the authentication hash, get the user’s profile info, set their session, and redirect to the appropriate route in the app.

Auth Guard

Now that we’ve implemented the authentication service, we have access to the properties and methods necessary to effectively use authentication state throughout our Angular application. Let’s use this logic to implement our AuthGuard for protecting routes.

Using the Angular CLI should have generated some helpful boilerplate code, and we only have to make a few minor changes to ensure that our guarded routes are only accessible to authenticated users.

Note: It’s important to note that route guards on their own do not confer sufficient security. You should always secure your API endpoints, as we have done in this tutorial, and never rely solely on the client side to authorize access to protected data.

Open the auth.guard.ts file and make the following changes:

 // src/app/auth/auth.guard.ts import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import { AuthService } from './auth.service'; @Injectable() export class AuthGuard implements CanActivate { constructor(private auth: AuthService) { } canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { if (this.auth.loggedIn) { return true; } else { // Send guarded route to redirect after logging in this.auth.login(state.url); return false; } } } 

We’ll import AuthService add a constructor() function to make the service available in our route guard. The canActivate() method should return true if conditions are met to grant access to a route, and false if not. In our case, the user should be able to access the guarded route if they are authenticated. The loggedIn property from our AuthService provides this information.

If the user does not have a valid token, we’ll prompt them to log in. We want them to be redirected back to the guarded route after they authenticate, so we’ll call the login() method and pass the guarded route ( state.url ) as the redirect parameter.

Note: Remember that we set up our entire app’s architecture and routing earlier. We already added AuthGuard to our dog details route, so it should be protected now that we’ve implemented the guard.

Core Logic

The last thing we’ll do in this section of our tutorial is build out the remaining components and services that belong to our CoreModule . We’ve already taken care of the LoadingComponent and ErrorComponent , so let’s move on to the header.

Компонент заголовка

The header will use methods and logic from our authentication service to show login and logout buttons as well as display the user’s name and picture if they’re authenticated. Open the header.component.ts file and add:

 // src/app/core/header/header.component.ts import { Component } from '@angular/core'; import { AuthService } from '../../auth/auth.service'; @Component({ selector: 'app-header', templateUrl: './header.component.html', styles: [` img { border-radius: 100px; width: 30px; } .loading { line-height: 31px; } .home-link { color: #212529; } .home-link:hover { text-decoration: none; } `] }) export class HeaderComponent { constructor(public auth: AuthService) {} } 

We’ll add a few simple styles and import our AuthService to make its members publicly available to our header component’s template.

Next open the header.component.html file and add:

 <!-- src/app/core/header/header.component.html --> <nav class="nav justify-content-between mt-2 mx-2 mb-3"> <div class="d-flex align-items-center"> <strong class="mr-1"><a routerLink="/" class="home-link">Popular Dogs </a></strong> </div> <div class="ml-3"> <small *ngIf="auth.loading" class="loading"> Logging in... </small> <ng-template [ngIf]="!auth.loading"> <button *ngIf="!auth.loggedIn" class="btn btn-primary btn-sm" (click)="auth.login()">Log In</button> <span *ngIf="auth.loggedIn"> <img [src]="auth.userProfile.picture"> <small>{{ auth.userProfile.name }}</small> <button class="btn btn-danger btn-sm" (click)="auth.logout()">Log Out</button> </span> </ng-template> </div> </nav> 

The header now shows:

  • The name of our app (“Popular Dogs”) with a link to the / route
  • A login button if the user is not authenticated
  • A “Logging in…” message if the user is currently authenticating
  • The user’s picture, name, and a logout button if the user is authenticated

Now that we have our header component built, we need to display it in our app.

Open the app.component.html file and add:

 <!-- src/app/app.component.html --> <app-header></app-header> <div class="container"> <router-outlet></router-outlet> </div> 

The header component will now be displayed in our app with the current routed component showing beneath it. Check it out in the browser and try logging in!

Dog and DogDetail Models

Let’s implement our dog.ts and dog-detail.ts interfaces . These are models that specify types for the shape of values that we’ll use in our app. Using models ensures that our data has the structure that we expect.

We’ll start with the dog.ts interface:

 // src/app/core/dog.ts export interface Dog { breed: string; rank: number; image: string; } 

Next let’s implement the dog-detail.ts interface:

 // src/app/core/dog-detail.ts export interface DogDetail { breed: string; rank: number; description: string; personality: string; energy: string; group: string; image: string; link: string; } 

API Service

With our Node API and models in place, we’re ready to implement the service that will call our API in the Angular front end.

Open the api.service.ts file and add this code:

 // src/app/core/api.service.ts import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http'; import { environment } from './../../environments/environment'; import { AuthService } from './../auth/auth.service'; import { Observable } from 'rxjs/Observable'; import { catchError } from 'rxjs/operators'; import 'rxjs/add/observable/throw'; import { Dog } from './../core/dog'; import { DogDetail } from './../core/dog-detail'; @Injectable() export class ApiService { private _API = `${environment.apiRoot}api`; constructor( private http: HttpClient, private auth: AuthService) { } getDogs$(): Observable<Dog[]> { return this.http .get(`${this._API}/dogs`) .pipe( catchError((err, caught) => this._onError(err, caught)) ); } getDogByRank$(rank: number): Observable<DogDetail> { return this.http .get(`${this._API}/dog/${rank}`, { headers: new HttpHeaders().set('Authorization', `Bearer ${this.auth.accessToken}`) }) .pipe( catchError((err, caught) => this._onError(err, caught)) ); } private _onError(err, caught) { let errorMsg = 'Error: Unable to complete request.'; if (err instanceof HttpErrorResponse) { errorMsg = err.message; if (err.status === 401 || errorMsg.indexOf('No JWT') > -1 || errorMsg.indexOf('Unauthorized') > -1) { this.auth.login(); } } return Observable.throw(errorMsg); } } 

We’ll add the necessary imports to handle HTTP in Angular along with the environment configuration, AuthService , RxJS imports, and Dog and DogDetail models we just created. We’ll set up private members for the _API and to store the _accessToken , then make the HttpClient and AuthService available privately to our API service.

Our API methods will return observables that emit one value when the API is either called successfully or an error is thrown. The getDogs$() stream returns an observable with an array of objects that are Dog -shaped. The getDogByRank$(rank) stream requires a numeric rank to be passed in, and will then call the API to retrieve the requested Dog ‘s data. This API call will send an Authorization header containing the authenticated user’s access token.

Finally, we’ll create an error handler that checks for errors and assesses if the user is not authenticated and prompts for login if so. The observable will then terminate with an error.

Note: We are using arrow functions to pass parameters to our handler functions for RxJS pipeable operators (such as catchError ). This is done to preserve the scope of the this keyword (see the “No separate this ” section of the MDN arrow functions documentation ).

Следующие шаги

We’ve already accomplished a lot in the first part of our tutorial series. In the next part, we’ll finish our Popular Dogs application. In the meantime, here are some additional resources that you may want to check out:

Angular Testing Resources

If you’re interested in learning more about testing in Angular, which we did not cover in this tutorial, please check out some of the following resources:

Дополнительные ресурсы

You can find more resources on Firebase, Auth0, and Angular here:

В следующей части нашего учебного пособия по Auth0 + Firebase + Angular мы покажем данные из нашего API собак и узнаем, как настроить и реализовать комментарии в реальном времени с помощью Firebase ! Проверьте Аутентификацию Firebase и Angular с Auth0: Часть 2 сейчас.