Статьи

Как создать рекордер локаций покемонов с помощью CouchDB

В предыдущей статье вы познакомились с CouchDB . На этот раз вы собираетесь создать полноценное приложение, в котором вы сможете применить полученные знания. Вы также узнаете, как защитить свою базу данных в конце урока.

Обзор проекта

Вы собираетесь построить регистратор мест появления покемонов.

Это позволит пользователям сохранять местоположения монстров, с которыми они сталкиваются на Pokemon Go. Карты Google будут использоваться для поиска местоположений и маркера для точного определения местоположения. Как только пользователь удовлетворен местоположением, с маркером можно взаимодействовать, когда он покажет модальное поле, которое позволяет пользователю ввести имя покемона и сохранить местоположение. Когда следующий пользователь приходит и ищет то же место, значения, добавленные предыдущими пользователями, будут нанесены на карту в виде маркеров. Вот как будет выглядеть приложение:

экран pokespawn

Полный исходный код проекта доступен на Github .

Настройка среды разработки

Если у вас нет хорошей изолированной среды разработки, рекомендуется использовать Homestead Improved .

Коробка не поставляется с установленной CouchDB, так что вам нужно будет сделать это вручную; но не просто CouchDB. Приложение должно работать с геоданными (широтами и долготами): вы предоставите CouchDB информацию о ограничивающем прямоугольнике из Google Maps. Ограничительная рамка представляет область, отображаемую в настоящее время на карте, и все предыдущие координаты, добавленные пользователями в эту область, будут также отображаться на карте. CouchDB не может сделать это по умолчанию, поэтому вам нужно установить плагин под названием GeoCouch , чтобы дать CouchDB некоторые пространственные суперспособности.

Самый простой способ сделать это с помощью док-контейнера GeoCouch . Вы также можете попробовать установить GeoCouch вручную, но для этого потребуется установить CouchDB из исходного кода и настроить все вручную. Я действительно не рекомендую этот метод, если у вас нет бороды Unix .

Идите вперед и установите Docker на виртуальную машину, которую вы используете , и вернитесь сюда, когда закончите.

Установка GeoCouch

Сначала клонируйте репо и перейдите в созданный каталог.

git clone [email protected]:elecnix/docker-geocouch.git cd docker-geocouch 

Затем откройте Dockerfile и замените скрипт для получения CouchDB следующим:

 # Get the CouchDB source RUN cd /opt; wget http://www-eu.apache.org/dist/couchdb/source/${COUCH_VERSION}/a$ tar xzf /opt/apache-couchdb-${COUCH_VERSION}.tar.gz 

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

Создайте образ докера:

 docker build -t elecnix/docker-geocouch:1.6.1 . 

Это займет некоторое время в зависимости от вашего интернет-соединения, так что перекусите. Как только это будет сделано, создайте контейнер и запустите его:

 docker create -ti -p 5984:5984 elecnix/docker-geocouch:1.6.1 docker start <container id> 

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

 curl localhost:5984 

Вне виртуальной машины, если вы правильно перенаправили порты, это будет:

 curl 192.168.33.10:5984 

Он должен вернуть следующее:

 {"couchdb":"Welcome","uuid":"2f0b5e00e9ce08996ace6e66ffc1dfa3","version":"1.6.1","vendor":{"version":"1.6.1","name":"The Apache Software Foundation"}} 

Обратите внимание, что я буду постоянно ссылаться на 192.168.33.10 протяжении всей статьи. Это IP-адрес, назначенный Scotchbox, который я и использовал. Если вы используете Homestead Improved , IP будет 192.168.10.10 . Вы можете использовать этот IP для доступа к приложению. Если вы используете что-то еще целиком, адаптируйтесь по мере необходимости.

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

Вы собираетесь использовать Slim Framework для ускорения разработки приложения. Создайте новый проект с помощью Composer :

 php composer create-project slim/slim-skeleton pokespawn 

pokespawn — это название проекта, поэтому перейдите к этому каталогу после завершения установки Composer. Затем установите следующие дополнительные пакеты:

 composer require danrovito/pokephp guzzlehttp/guzzle gregwar/image vlucas/phpdotenv 

Вот краткий обзор каждого из них:

  • danrovito/pokephp — для простого общения с API Pokemon.
  • guzzlehttp/guzzle — для guzzlehttp/guzzle запросов на сервер CouchDB.
  • gregwar/image — для изменения размера спрайтов Pokemon, возвращаемых API Pokemon.
  • vlucas/phpdotenv — для хранения значений конфигурации.

Настройка базы данных

Получите доступ к Futon из браузера и создайте новую базу данных под названием pokespawn . После создания зайдите в базу данных и создайте новый вид. Вы можете сделать это, нажав на выпадающий список и выбрав временный вид . Добавьте следующее внутри текстовой области для функции карты :

 function(doc){ if(doc.doc_type == 'pokemon'){ emit(doc.name, null); } } 

создать новый вид

После этого нажмите кнопку « Сохранить как» , добавьте pokemon в качестве имени документа проекта и by_name качестве имени представления. Нажмите на сохранить, чтобы сохранить вид. Позже вы будете использовать это представление, чтобы предлагать имена покемонов в зависимости от того, что ввел пользователь.

сохранить вид

Затем создайте проектный документ для ответа на пространственные поиски. Вы можете сделать это, выбрав Дизайн документов в раскрывающемся списке, а затем нажмите на новый документ . Находясь на странице для создания проектного документа, нажмите кнопку « Добавить поле» и добавьте spatial качестве имени поля, а следующее в качестве значения:

 { "points": "function(doc) {\n if (doc.loc) {\n emit([{\n type: \"Point\",\n coordinates: [doc.loc[0], doc.loc[1]]\n }], [doc.name, doc.sprite]);\n }};" } 

Этот проектный документ использует пространственные функции, предоставляемые GeoCouch. Первое, что он делает, это проверяет, есть ли в документе поле loc . Поле loc представляет собой массив, содержащий координаты определенного местоположения, причем первый элемент содержит широту, а второй элемент содержит долготу. Если документ соответствует этим критериям, он использует функцию emit() как и обычный вид. key — это геометрия GeoJSON, а значение — это массив, содержащий имя покемона и спрайт.

Когда вы делаете запрос к документу проекта, вам нужно указать start_range и end_range которые имеют формат массива JSON. Каждый элемент может быть числом или null . null используется, если вы хотите открытый диапазон . Вот пример запроса:

 curl -X GET --globoff 'http://192.168.33.10:5984/pokespawn/_design/location/_spatial/points?start_range=[-33.87049924568689,151.2149563379288]&end_range=[33.86709181198735,151.22298150730137]' 

И его вывод:

 { "update_seq": 289, "rows":[{ "id":"c8cc500c68f679a6949a7ff981005729", "key":[ [ -33.869107336588, -33.869107336588 ], [ 151.21772705984, 151.21772705984 ] ], "bbox":[ -33.869107336588, 151.21772705984, -33.869107336588, 151.21772705984 ], "geometry":{ "type":"Point", "coordinates":[ -33.869107336588, 151.21772705984 ] }, "value":[ "snorlax", "143.png" ] }] } 

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

Создание проекта

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

Poke Importer

Приложению требуется, чтобы некоторые данные Pokemon уже были в базе данных, прежде чем их можно будет использовать, поэтому необходим скрипт, который выполняется только локально. Создайте файл poke-importer.php в корневом каталоге вашего проекта и добавьте следующее:

 <?php require 'vendor/autoload.php'; set_time_limit(0); use PokePHP\PokeApi; use Gregwar\Image\Image; $api = new PokeApi; $client = new GuzzleHttp\Client(['base_uri' => 'http://192.168.33.10:5984']); //create a client for talking to CouchDB $pokemons = $api->pokedex(2); //make a request to the API $pokemon_data = json_decode($pokemons); //convert the json response to array foreach ($pokemon_data->pokemon_entries as $row) { $pokemon = [ 'id' => $row->entry_number, 'name' => $row->pokemon_species->name, 'sprite' => "{$row->entry_number}.png", 'doc_type' => "pokemon" ]; //get image from source, save it then resize. Image::open("https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/{$row->entry_number}.png") ->resize(50, 50) ->save('public/img/' . $row->entry_number . '.png'); //save the pokemon data to the database $client->request('POST', "/pokespawn", [ 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => json_encode($pokemon) ]); echo $row->pokemon_species->name . "\n"; } echo "done!"; 

Этот скрипт выполняет запрос к конечной точке Pokedex API Pokemon . Для этой конечной точки требуется идентификатор версии Pokedex, которую вы хотите вернуть. Поскольку Pokemon Go только в настоящее время позволяет игрокам ловить покемонов из первого поколения, укажите 2 в качестве идентификатора. Это возвращает всех покемонов из региона Канто в оригинальной игре про покемонов. Затем переберите данные, извлеките всю необходимую информацию, сохраните спрайт и создайте новый документ, используя извлеченные данные.

Маршруты

Откройте файл src/routes.php и добавьте следующие маршруты:

 <?php $app->get('/', 'HomeController:index'); $app->get('/search', 'HomeController:search'); $app->post('/save-location', 'HomeController:saveLocation'); $app->post('/fetch', 'HomeController:fetch'); 

Каждый из маршрутов будет реагировать на действия, которые могут быть выполнены во всем приложении. Корневой маршрут возвращает домашнюю страницу, search маршрут возвращает подсказки имен Pokemon, маршрут save-location сохраняет местоположение, а маршрут fetch возвращает Pokemon в определенном месте.

Домашний контроллер

В каталоге src создайте папку app/Controllers и внутри создайте файл HomeController.php . Это выполнит все действия, необходимые для каждого из маршрутов. Вот код:

 <?php namespace App\Controllers; class HomeController { protected $renderer; public function __construct($renderer) { $this->renderer = $renderer; //the twig renderer $this->db = new \App\Utils\DB; //custom class for talking to couchdb } public function index($request, $response, $args) { //render the home page return $this->renderer->render($response, 'index.html', $args); } public function search() { $name = $_GET['name']; //name of the pokemon being searched return $this->db->searchPokemon($name); //returns an array of suggestions based on the user input } public function saveLocation() { $id = $_POST['pokemon_id']; //the ID assigned by CouchDB to the Pokemon return $this->db->savePokemonLocation($id, $_POST['pokemon_lat'], $_POST['pokemon_lng']); //saves the pokemon location to CouchDB and returns the data needed to plot the pokemon in the map } public function fetch() { return json_encode($this->db->fetchPokemons($_POST['north_east'], $_POST['south_west'])); //returns the pokemon's within the bounding box of Google map. } } 

Домашний контроллер использует $renderer который передается через конструктор для визуализации домашней страницы приложения. Он также использует класс DB который вы вскоре создадите.

Разговор с CouchDB

Создайте файл Utils/DB.php в каталоге app . Откройте файл и создайте класс:

 <?php namespace App\Utils; class DB { } 

Внутри класса создайте новый клиент Guzzle. Вы используете Guzzle вместо некоторых PHP-клиентов для CouchDB, потому что вы можете делать с ним все, что захотите.

 private $client; public function __construct() { $this->client = new \GuzzleHttp\Client([ 'base_uri' => getenv('BASE_URI') ]); } 

Конфигурация из файла .env в корне проекта. Это содержит базовый URL CouchDB.

 BASE_URI="http://192.168.33.10:5984" 

searchPokemon отвечает за возврат данных, используемых функцией автоматического предложения. Поскольку CouchDB на самом деле не поддерживает условие LIKE которому вы привыкли в SQL, вы используете небольшой хак, чтобы имитировать его. Хитрость здесь заключается в использовании start_key и end_key вместо просто key который возвращает только точные совпадения. fff0 — это один из специальных символов юникода, размещенных в самом конце базовой многоязычной плоскости. Это делает его хорошим кандидатом для добавления в конец искомой строки, что делает остальные символы необязательными из-за ее высокого значения. Обратите внимание, что этот хак работает только для коротких слов, поэтому его более чем достаточно для поиска имен покемонов.

 public function searchPokemon($name) { $unicode_char = '\ufff0'; $data = [ 'include_docs' => 'true', 'start_key' => '"' . $name . '"', 'end_key' => '"' . $name . json_decode('"' . $unicode_char .'"') . '"' ]; //make a request to the view you created earlier $doc = $this->makeGetRequest('/pokespawn/_design/pokemon/_view/by_name', $data); if (count($doc->rows) > 0) { $data = []; foreach ($doc->rows as $row) { $data[] = [ $row->key, $row->id ]; } return json_encode($data); } $result = ['no_result' => true]; return json_encode($result); } 

makeGetRequest используется для выполнения запросов на чтение к CouchDB и makePostRequest для записи.

 public function makeGetRequest($endpoint, $data = []) { if (!empty($data)) { //make a GET request to the endpoint specified, with the $data passed in as a query parameter $response = $this->client->request('GET', $endpoint, [ 'query' => $data ]); } else { $response = $this->client->request('GET', $endpoint); } return $this->handleResponse($response); } private function makePostRequest($endpoint, $data) { //make a POST request to the endpoint specified, passing in the $data for the request body $response = $this->client->request('POST', $endpoint, [ 'headers' => [ 'Content-Type' => 'application/json' ], 'body' => json_encode($data) ]); return $this->handleResponse($response); } 

savePokemonLocation сохраняет координаты, на которые в данный момент указывает маркер карты Google, а также name и sprite . Поле doc_type также добавлено для легкого поиска всех документов, связанных с местоположениями.

 public function savePokemonLocation($id, $lat, $lng) { $pokemon = $this->makeGetRequest("/pokespawn/{$id}"); //get pokemon details based on ID //check if supplied data are valid if (!empty($pokemon->name) && $this->isValidCoordinates($lat, $lng)) { $lat = (double) $lat; $lng = (double) $lng; //construct the data to be saved to the database $data = [ 'name' => $pokemon->name, 'sprite' => $pokemon->sprite, 'loc' => [$lat, $lng], 'doc_type' => 'pokemon_location' ]; $this->makePostRequest('/pokespawn', $data); //save the location data $pokemon_data = [ 'type' => 'ok', 'lat' => $lat, 'lng' => $lng, 'name' => $pokemon->name, 'sprite' => $pokemon->sprite ]; return json_encode($pokemon_data); //return the data needed by the pokemon marker } return json_encode(['type' => 'fail']); //invalid data } 

isValidCoordinates проверяет, имеют ли значения широты и долготы допустимый формат.

 private function isValidCoordinates($lat = '', $lng = '') { $coords_pattern = '/^[+\-]?[0-9]{1,3}\.[0-9]{3,}\z/'; if (preg_match($coords_pattern, $lat) && preg_match($coords_pattern, $lng)) { return true; } return false; } 

fetchPokemons — это функция, которая отправляет запрос в проектный документ для пространственного поиска, который вы создали ранее. Здесь вы указываете юго-западные координаты в качестве значения для start_range а северо-восточные координаты — в качестве значения для end_range . Ответ также ограничен первыми 100 строками, чтобы не запрашивать слишком много данных. Ранее вы также видели, что CouchDB возвращает некоторые данные, которые на самом деле не нужны. Было бы полезно извлечь, а затем вернуть только те данные, которые необходимы для внешнего интерфейса. Я решил оставить это как оптимизацию на другой день.

 public function fetchPokemons($north_east, $south_west) { $north_east = array_map('doubleval', $north_east); //convert all array items to double $south_west = array_map('doubleval', $south_west); $data = [ 'start_range' => json_encode($south_west), 'end_range' => json_encode($north_east), 'limit' => 100 ]; $pokemons = $this->makeGetRequest('/pokespawn/_design/location/_spatial/points', $data); //fetch all pokemon's that are in the current area return $pokemons; } 

handleResponse преобразует строку JSON, возвращенную CouchDB, в массив.

 private function handleResponse($response) { $doc = json_decode($response->getBody()->getContents()); return $doc; } 

Откройте composer.json в корневом каталоге и добавьте следующее прямо под свойством require , затем выполните composer dump-autoload . Это позволяет вам автоматически загружать все файлы в каталоге src/app и делать его доступным в пространстве имен App :

 "autoload": { "psr-4": { "App\\": "src/app" } } 

Наконец, введите Home Controller в контейнер. Вы можете сделать это, открыв файл src/dependencies.php и добавив следующее:

 $container['HomeController'] = function ($c) { return new App\Controllers\HomeController($c->renderer); }; 

Это позволяет передавать средство визуализации Twig в Home Controller и делает HomeController доступным с маршрутизатора.

Шаблон домашней страницы

Теперь вы готовы перейти к интерфейсу. Сначала создайте файл templates/index.html в корне каталога проекта и добавьте следующее:

 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>PokéSpawn</title> <link rel="stylesheet" href="lib/picnic/picnic.min.css"> <link rel="stylesheet" href="lib/remodal/dist/remodal.css"> <link rel="stylesheet" href="lib/remodal/dist/remodal-default-theme.css"> <link rel="stylesheet" href="lib/javascript-auto-complete/auto-complete.css"> <link rel="stylesheet" href="css/style.css"> <link rel="icon" href="favicon.ico"><!-- by Maicol Torti https://www.iconfinder.com/Maicol-Torti --> </head> <body> <div id="header"> <div id="title"> <img src="img/logo.png" alt="logo" class="header-item" /> <h1 class="header-item">PokéSpawn</h1> </div> <input type="text" id="place" class="controls" placeholder="Where are you?"><!-- text field for typing the location --> </div> <div id="map"></div> <!-- modal for saving pokemon location --> <div id="add-pokemon" class="remodal" data-remodal-id="modal"> <h3>Plot Pokémon Location</h3> <form method="POST" id="add-pokemon-form"> <div> <input type="hidden" name="pokemon_id" id="pokemon_id"><!-- id of the pokemon in CouchDB--> <input type="hidden" name="pokemon_lat" id="pokemon_lat"><!--latitude of the red marker --> <input type="hidden" name="pokemon_lng" id="pokemon_lng"><!--longitude of the red marker --> <input type="text" name="pokemon_name" id="pokemon_name" placeholder="Pokémon name"><!--name of the pokemon whose location is being added --> </div> <div> <button type="button" id="save-location">Save Location</button><!-- trigger the submission of location to CouchDB --> </div> </form> </div> <script src="lib/zepto.js/dist/zepto.min.js"></script><!-- event listening, ajax --> <script src="lib/remodal/dist/remodal.min.js"></script><!-- for modal box --> <script src="lib/javascript-auto-complete/auto-complete.min.js"></script><!-- for autocomplete text field --> <script src="js/main.js"></script> <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_GOOGLEMAP_APIKEY&callback=initMap&libraries=places" defer></script><!-- for showing a map--> </body> </html> 

В <head> представлены стили из различных библиотек, которые использует приложение, а также стили для приложения. В <body> находятся текстовое поле для поиска местоположений, контейнер карты и модальное поле для сохранения нового местоположения. Ниже приведены сценарии, используемые в приложении. Не забудьте заменить YOUR_GOOGLEMAP_APIKEY в YOUR_GOOGLEMAP_APIKEY Google Maps своим собственным ключом API.

JavaScript

Для основного файла JavaScript ( public/js/main.js ) сначала создайте переменные для хранения значений, которые вам понадобятся во всем файле.

 var modal = $('#add-pokemon').remodal(); //initialize modal var map; //the google map var markers = []; //an array for storing all the pokemon markers currently plotted in the map 

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

 function initMap() { var min_zoomlevel = 18; map = new google.maps.Map(document.getElementById('map'), { center: {lat: -33.8688, lng: 151.2195}, //set disableDefaultUI: true, //hide default UI controls zoom: min_zoomlevel, //set default zoom level mapTypeId: 'roadmap' //set type of map }); //continue here... } 

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

 marker = new google.maps.Marker({ map: map, position: map.getCenter(), draggable: true }); marker.addListener('click', function(){ var position = marker.getPosition(); $('#pokemon_lat').val(position.lat()); $('#pokemon_lng').val(position.lng()); modal.open(); }); 

Инициализируйте окно поиска:

 var header = document.getElementById('header'); var input = document.getElementById('place'); var searchBox = new google.maps.places.SearchBox(input); //create a google map search box map.controls[google.maps.ControlPosition.TOP_LEFT].push(header); //position the header at the top left side of the screen 

Добавить различных слушателей карты:

 map.addListener('bounds_changed', function() { //executes when user drags the map searchBox.setBounds(map.getBounds()); //make places inside the current area a priority when searching }); map.addListener('zoom_changed', function() { //executes when user zooms in or out of the map //immediately set the zoom to the minimum zoom level if the current zoom goes over the minimum if (map.getZoom() < min_zoomlevel) map.setZoom(min_zoomlevel); }); map.addListener('dragend', function() { //executes the moment after the map has been dragged //loop through all the pokemon markers and remove them from the map markers.forEach(function(marker) { marker.setMap(null); }); markers = []; marker.setPosition(map.getCenter()); //always place the marker at the center of the map fetchPokemon(); //fetch some pokemon in the current viewable area }); 

Добавьте прослушиватель событий, когда место в окне поиска меняется.

 searchBox.addListener('places_changed', function() { //executes when the place in the searchbox changes var places = searchBox.getPlaces(); if (places.length == 0) { return; } var bounds = new google.maps.LatLngBounds(); var place = places[0]; //only get the first place if (!place.geometry) { return; } marker.setPosition(place.geometry.location); //put the marker at the location being searched if (place.geometry.viewport) { // only geocodes have viewport bounds.union(place.geometry.viewport); } else { bounds.extend(place.geometry.location); } map.fitBounds(bounds); //adjust the current map bounds to that of the place being searched fetchPokemon(); //fetch some Pokemon in the current viewable area }); 

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

 function fetchPokemon(){ //get the northeast and southwest coordinates of the viewable area of the map var bounds = map.getBounds(); var north_east = [bounds.getNorthEast().lat(), bounds.getNorthEast().lng()]; var south_west = [bounds.getSouthWest().lat(), bounds.getSouthWest().lng()]; $.post( '/fetch', { north_east: north_east, south_west: south_west }, function(response){ var response = JSON.parse(response); response.rows.forEach(function(row){ //loop through all the results returned var position = new google.maps.LatLng(row.geometry.coordinates[0], row.geometry.coordinates[1]); //create a new google map position //create a new marker using the position created above var poke_marker = new google.maps.Marker({ map: map, title: row.value[0], //name of the pokemon position: position, icon: 'img/' + row.value[1] //pokemon image that was saved locally }); //create an infowindow for the marker var infowindow = new google.maps.InfoWindow({ content: "<strong>" + row.value[0] + "</strong>" }); //when clicked it will show the name of the pokemon poke_marker.addListener('click', function() { infowindow.open(map, poke_marker); }); markers.push(poke_marker); }); } ); } 

Это код для добавления функции автоматического предложения текстового поля для ввода имени покемона. Функция renderItem указана для настройки HTML-кода, используемого для отображения каждого предложения. Это позволяет вам добавить идентификатор покемона в качестве атрибута данных, который вы затем используете для установки значения поля pokemon_id после выбора предложения.

 new autoComplete({ selector: '#pokemon_name', //the text field to add the auto-complete source: function(term, response){ //use the results returned by the search route as a data source $.getJSON('/search?name=' + term, function(data){ response(data); }); }, renderItem: function (item, search){ //the code for rendering each suggestions. search = search.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); var re = new RegExp("(" + search.split(' ').join('|') + ")", "gi"); return '<div class="autocomplete-suggestion" data-id="' + item[1] + '" data-val="' + item[0] + '">' + item[0].replace(re, "<b>$1</b>")+'</div>'; }, onSelect: function(e, term, item){ //executed when a suggestion is selected $('#pokemon_id').val(item.getAttribute('data-id')); } }); 

При нажатии кнопки « Сохранить местоположение» на сервер делается запрос на добавление местоположения Pokemon в CouchDB.

 $('#save-location').click(function(e){ $.post('/save-location', $('#add-pokemon-form').serialize(), function(response){ var data = JSON.parse(response); if(data.type == 'ok'){ var position = new google.maps.LatLng(data.lat, data.lng); //create a location //create a new marker and use the location var poke_marker = new google.maps.Marker({ map: map, title: data.name, //name of the pokemon position: position, icon: 'img/' + data.sprite //pokemon image }); //create an infowindow for showing the name of the pokemon var infowindow = new google.maps.InfoWindow({ content: "<strong>" + data.name + "</strong>" }); //show name of pokemon when marker is clicked poke_marker.addListener('click', function() { infowindow.open(map, poke_marker); }); markers.push(poke_marker); } modal.close(); $('#pokemon_id, #pokemon_lat, #pokemon_lng, #pokemon_name').val(''); //reset the form }); }); $('#add-pokemon-form').submit(function(e){ e.preventDefault(); //prevent the form from being submited on enter }) 

Стили

Создайте файл public/css/styles.css и добавьте следующие стили:

 html, body { height: 100%; margin: 0; padding: 0; } #header { text-align: center; } #title { float: left; padding: 5px; color: #f5716a; } .header-item { padding-top: 10px; } h1.header-item { font-size: 14px; margin: 0; padding: 0; } #map { height: 100%; } .controls { margin-top: 10px; border: 1px solid transparent; border-radius: 2px 0 0 2px; box-sizing: border-box; -moz-box-sizing: border-box; height: 32px; outline: none; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); } #place { background-color: #fff; margin-left: 12px; padding: 0 11px 0 13px; text-overflow: ellipsis; width: 300px; margin-top: 20px; } #place:focus { border-color: #4d90fe; } #type-selector { color: #fff; background-color: #4d90fe; padding: 5px 11px 0px 11px; } #type-selector label { font-family: Roboto; font-size: 13px; font-weight: 300; } #target { width: 345px; } .remodal-wrapper { z-index: 100; } .remodal-overlay { z-index: 100; } 

Безопасность CouchDB

По умолчанию CouchDB открыт для всех. Это означает, что, как только вы выставите его в Интернет, любой может нанести ущерб вашей базе данных. Любой может выполнить любую операцию с базой данных, просто используя Curl , Postman или любой другой инструмент для выполнения HTTP-запросов. На самом деле это временное государство даже имеет название: «партия администратора». Вы видели это в действии в предыдущем уроке и даже когда ранее создавали новую базу данных, представление и проектный документ. Все эти действия могут быть выполнены только администратором сервера, но вы сделали это без входа в систему или чего-либо еще. Все еще не убежден? Попробуйте выполнить это на вашем локальном компьютере:

 curl -X PUT http://192.168.33.10:5984/my_newdatabase 

В ответ вы получите следующее, если у вас еще нет администратора сервера в вашей установке CouchDB:

 {"ok":true} 

Да, верно? Хорошая новость в том, что это легко исправить. Все, что вам нужно сделать, это создать администратора сервера. Вы можете сделать это с помощью следующей команды:

 curl -X PUT http://192.168.33.10:5984/_config/admins/kami -d '"mysupersecurepassword"' 

Приведенная выше команда создает нового администратора сервера с именем «kami» с паролем «mysupersecurepassword».

По умолчанию CouchDB не имеет администратора сервера, поэтому после его создания вечеринка администратора заканчивается. Обратите внимание, что администраторы сервера обладают богоподобными способностями, поэтому вам, вероятно, лучше создать только один или два. Затем создайте несколько администраторов баз данных, которые могут выполнять только операции CRUD. Вы можете сделать это, выполнив следующую команду:

 curl -HContent-Type:application/json -vXPUT http://kami:[email protected]:5984/_users/org.couchdb.user:plebian --data-binary '{"_id": "org.couchdb.user:plebian","name": "plebian","roles": [],"type": "user","password": "mypass"}' 

В случае успеха он вернет ответ, подобный следующему:

 * Trying 192.168.33.10... * Connected to 192.168.33.10 (192.168.33.10) port 5984 (#0) * Server auth using Basic with user 'root' > PUT /_users/org.couchdb.user:plebian HTTP/1.1 > Host: 192.168.33.10:5984 > Authorization: Basic cm9vdDpteXN1cGVyc2VjdXJlcGFzc3dvcmQ= > User-Agent: curl/7.47.0 > Accept: */* > Content-Type:application/json > Content-Length: 101 > * upload completely sent off: 101 out of 101 bytes < HTTP/1.1 201 Created < Server: CouchDB/1.6.1 (Erlang OTP/R16B03) < Location: http://192.168.33.10:5984/_users/org.couchdb.user:plebian < ETag: "1-9c4abdc905ecdc9f0f56921d7de915b9" < Date: Thu, 18 Aug 2016 07:57:20 GMT < Content-Type: text/plain; charset=utf-8 < Content-Length: 87 < Cache-Control: must-revalidate < {"ok":true,"id":"org.couchdb.user:plebian","rev":"1-9c4abdc905ecdc9f0f56921d7de915b9"} * Connection #0 to host 192.168.33.10 left intact 

Теперь вы можете попробовать ту же команду ранее с другим именем базы данных:

 curl -X PUT http://192.168.33.10:5984/my_awesomedatabase 

И CouchDB будет кричать на тебя:

 {"error":"unauthorized","reason":"You are not a server admin."} 

Чтобы это работало, теперь вы должны указать свое имя пользователя и пароль в URL:

 curl -X PUT http://{your_username}:{your_password}@192.168.33.10:5984/my_awesomedatabase 

Хорошо, вот и все? Ну, не совсем, потому что единственное, что вы сделали, это ограничили операции с базой данных, которые могут выполнять только администраторы сервера. Это включает в себя такие вещи, как создание новой базы данных, удаление базы данных, управление пользователями, полный доступ администратора ко всем базам данных (включая системные таблицы), операции CRUD со всеми документами. Это оставляет вам не прошедших проверку подлинности пользователей, которые по-прежнему имеют возможность делать CRUD-файлы в любой базе данных. Вы можете попробовать это, выйдя из Futon, выбрав любую базу данных, с которой хотите поработать, и сделайте в ней CRUD. CouchDB все равно с удовольствием выполнит эти операции за вас.

Итак, как вы исправляете оставшиеся дыры? Вы можете сделать это, создав документ проекта, который проверит, совпадает ли имя пользователя пользователя, который пытается выполнить операцию записи (вставка или обновление), с именем пользователя, которому разрешено это делать. В Futon войдите в систему, используя учетную запись администратора сервера или администратора базы данных, выберите базу данных, с которой вы хотите работать, и создайте новый проектный документ. Установите идентификатор как _design/blockAnonymousWrites , добавьте поле с именем validate_doc_update и установите следующее значение:

 function(new_doc, old_doc, userCtx){ if(userCtx.name != 'kami'){ throw({forbidden: "Not Authorized"}); } } 

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

secObj также передается в качестве четвертого аргумента, но вам не нужно над ним работать; вот почему это опущено. В основном, secObj описывает, какие привилегии администратора были установлены в базе данных.

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

блокировать анонимные записи

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

Кроме того, если у вас много пользователей в одной базе данных, вы также можете проверить роль. Функция ниже выдаст ошибку любому пользователю, у которого нет роли «pokemon_master».

 function(new_doc, old_doc, userCtx) { if(userCtx.roles.indexOf('pokemon_master') == -1){ throw({forbidden: "Not Authorized"}); } } 

Если вы хотите узнать больше о том, как защитить CouchDB, обязательно ознакомьтесь со следующими ресурсами:

Обеспечение безопасности приложения

Подведем итоги, обновив приложение для использования мер безопасности, которые вы применили к базе данных. Сначала обновите файл .env : измените BASE_URI только IP-адрес и порт, а затем добавьте имя пользователя и пароль созданного вами пользователя CouchDB.

 BASE_URI="192.168.33.10:5984" COUCH_USER="plebian" COUCH_PASS="mypass" 

Затем обновите конструктор класса DB чтобы использовать новые детали:

 public function __construct() { $this->client = new \GuzzleHttp\Client([ 'base_uri' => 'http://' . getenv('COUCH_USER') . ':' . getenv('COUCH_PASS') . '@' . getenv('BASE_URI') ]); } 

Вывод

Это оно! В этом уроке вы узнали, как создать приложение для записи местоположения мест возрождения Pokemon с помощью CouchDB. С помощью плагина GeoCouch вы смогли выполнить пространственные запросы и узнали, как защитить базу данных CouchDB.

Используете ли вы CouchDB в своих проектах? Зачем? Какие-нибудь предложения / особенности, чтобы добавить в этот маленький наш проект? Дайте нам знать об этом в комментариях!