Это руководство является частью серии « Создай свой стартап с помощью PHP» на Envato Tuts +. В этой серии я проведу вас через запуск стартапа от концепции до реальности, используя мое приложение Meeting Planner в качестве примера из реальной жизни. На каждом этапе я буду публиковать код Планировщика собраний в качестве примеров с открытым исходным кодом, из которых вы можете извлечь уроки. Я также буду решать вопросы, связанные с бизнесом по мере их возникновения.
Запланируйте групповое собрание с помощью ярлыка URL
Добро пожаловать! Я недавно вернулся из своего любимого места в мире, о котором я упоминал в конце последнего эпизода . После завершения нескольких встреч участников , я взял перерыв в природе.
Сегодня я добавлю возможность приглашать участников собрания, предоставляя безопасный URL-адрес, связанный с вашей встречей. Это будет особенно полезно для планирования групповых встреч. Например, если вы хотите пригласить 30 человек, иногда проще отправить электронное письмо всем с URL-адресом приглашения.
Если вы еще этого не сделали, попробуйте назначить встречу своей группы сегодня ! Пригласите нескольких друзей встретиться с вами для чайного гриба, кавы или кофе. Поделитесь своими мыслями и отзывами об опыте каждого в комментариях ниже. Я участвую в обсуждениях, но вы также можете связаться со мной @reifman в Twitter. Я всегда открыт для новых идей для планировщика собраний, а также предложений для будущих серий.
Напоминаем, что весь код для Meeting Planner предоставляется с открытым исходным кодом и написан на Yii2 Framework для PHP. Если вы хотите узнать больше о Yii2, ознакомьтесь с моей параллельной серией Программирование с Yii2 .
Прежде чем углубиться в эту функцию, я хочу привести несколько примеров некоторых типичных дневных ошибок (или упущений), с которыми я столкнулся при создании сервиса. (Если вы просто хотите прочитать о безопасных общедоступных URL-адресах, пожалуйста, пропустите этот раздел.)
Startup Bug Interlude
Поскольку все больше и больше людей начинают пробовать Meeting Planner, появляются ошибки. И часто во время разработки я замечаю их сам. Вот несколько последних, просто чтобы дать вам представление о жизни стартапа.
Может быть трудно сосредоточиться на развитии бизнеса и маркетинге, кодировании новых функций, выявлении и исправлении ошибок. Запуск одного человека возрастает с ростом возможностей вашего сайта.
Как я уже писал ранее, в настоящее время я использую Asana для планирования функций, а также для отслеживания ошибок.
Ошибка назначения
Я уверен, что если бы я имел больше опыта как разработчик, работал с коллегами или имел больше времени, чтобы не программировать Meeting Planner, я бы точно знал, какое расширение Atom Editor ищет для них. Если вы знаете, пожалуйста, напишите об этом в комментариях.
По-видимому, в важной функции проверки того, действительно ли зритель был участником собрания, я использовал назначение для проверки. Другими словами, я не спрашивал, был ли владелец зрителем — я временно делал это.
1
2
3
4
5
6
7
|
public static function isAttendee($meeting_id,$user_id) {
$m = Meeting::findOne($meeting_id);
// are they the organizer?
// EEEK!
if ($m->owner_id = $user_id) {
return true;
}
|
Вы помните, два равных — это сравнение, одно равное — это задание. Точно так же, как точки — для конкатенаций, а знаки плюс — для сложения, за исключением JavaScript, где очень трудно найти ошибки (именно поэтому Ajax ад в PHP).
Запросы к базе данных, которые со временем не выполняются
По мере того, как увеличивалось мое использование Meeting Planner, в моих представлениях с вкладками появлялось все больше и больше встреч. А потом я заметил, что иногда появляются дубликаты. Это было трудно обнаружить раньше, когда было меньше данных.
Мои специфичные для вкладок запросы на собрания (т. Е. Планирование собрания, подтвержденные собрания, прошедшие собрания и т. Д.) Не изолировали уникальные записи:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
$planningProvider = new ActiveDataProvider([
‘query’ => Meeting::find()->joinWith(‘participants’)
->where([‘owner_id’=>Yii::$app->user->getId()])
->orWhere([‘participant_id’=>Yii::$app->user->getId()])
->andWhere([‘meeting.status’=>[Meeting::STATUS_PLANNING,Meeting::STATUS_SENT]])
/* NEEDED TO ADD THIS */
->distinct(),
‘sort’=> [‘defaultOrder’ => [‘created_at’=>SORT_DESC]],
‘pagination’ => [
‘pageSize’ => 7,
‘params’ => array_merge($_GET, [‘tab’ => ‘planning’]),
],
]);
|
Добавление ->distinct()
к запросу исправило это.
Yii2 Нумерация страниц в виде сетки на вкладке
Еще одна ошибка, с которой я столкнулся с большим количеством данных, заключалась в том, что ссылки на страницы Yii2 всегда возвращали меня на первую вкладку.
Я добавил параметр запроса для текущей вкладки, которую теперь ищет MeetingController.php actionIndex
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
public function actionIndex()
{
if (Meeting::countUserMeetings(Yii::$app->user->getId())==0) {
$this->redirect([‘create’]);
}
$tab =’planning’;
if (isset(Yii::$app->request->queryParams[‘tab’])) {
$tab =Yii::$app->request->queryParams[‘tab’];
}
$planningProvider = new ActiveDataProvider([
‘query’ => Meeting::find()->joinWith(‘participants’)->where([‘owner_id’=>Yii::$app->user->getId()])->orWhere([‘participant_id’=>Yii::$app->user->getId()])->andWhere([‘meeting.status’=>[Meeting::STATUS_PLANNING,Meeting::STATUS_SENT]])->distinct(),
‘sort’=> [‘defaultOrder’ => [‘created_at’=>SORT_DESC]],
‘pagination’ => [
‘pageSize’ => 7,
‘params’ => array_merge($_GET, [‘tab’ => ‘planning’]),
],
]);
|
Также я дал указание параметрам pagination params
объединить текущую настройку вкладки. Когда пользователи нажимают на другую ссылку на страницу, текущая вкладка теперь включена.
Наконец, я также сообщил об этом /frontend/views/meeting/index.php, установив активную вкладку из параметра запроса:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
<!— Tab panes —>
<div class=»tab-content»>
<div class=»tab-pane <?= ($tab==’planning’?’active’:») ?>» id=»planning»>
<div class=»meeting-index»>
<?php
?>
<?= $this->render(‘_grid’, [
‘mode’=>’planning’,
‘dataProvider’ => $planningProvider,
‘timezone’=>$timezone,
]) ?>
</div> <!— end of planning meetings tab —>
</div>
<div class=»tab-pane <?= ($tab==’upcoming’?’active’:») ?>» id=»upcoming»>
<div class=»meeting-index»>
<?= $this->render(‘_grid’, [
‘mode’=>’upcoming’,
‘dataProvider’ => $upcomingProvider,
‘timezone’=>$timezone,
]) ?>
</div> <!— end of upcoming meetings tab —>
</div>
<div class=»tab-pane <?= ($tab==’past’?’active’:») ?>» id=»past»>
<?= $this->render(‘_grid’, [
‘mode’=>’past’,
‘dataProvider’ => $pastProvider,
‘timezone’=>$timezone,
]) ?>
</div> <!— end of past meetings tab —>
|
Это всего лишь несколько хороших примеров повседневных ошибок, с которыми я сталкиваюсь при создании стартапа в рамках Meeting Planner.
Теперь давайте погрузимся в создание приглашений с помощью ярлыков URL, как и обещали.
Создание безопасных общедоступных ярлыков URL
Думая о безопасности для URL
Чтобы случайному потоку предположений URL-адресов было сложнее проникнуть в чье-либо приглашение на собрание, мне нужно было иметь достаточно уникальный ключ в сочетании с неуязвимым кодом.
Я решил использовать имя пользователя человека в качестве ключа. У каждого пользователя будет большое количество почти неосуществимых кодов собраний.
Так, например, URL собрания может быть https://meetingplanner.io/presidenthillary/X1Y2Z3A7C9
.
Для кода я решил использовать восемь буквенно-цифровых символов с учетом регистра. Другими словами, каждый символ будет az, AZ или 0-9, по существу 62 варианта для каждого символа.
Общее количество возможностей для каждого пользователя составляет 218 340 105 584 896 — более 218 трлн. О, и вы должны знать имя пользователя вашей цели, чтобы начать! Было бы намного проще взломать учетную запись участника электронной почты.
Добавление кода безопасности для каждой встречи
Чтобы добавить код безопасности для всех существующих собраний, я создал миграцию m160902_174350_extend_meeting_for_identifier.php:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class m160902_174350_extend_meeting_for_identifier extends Migration
{
public function up()
{
$tableOptions = null;
if ($this->db->driverName === ‘mysql’) {
$tableOptions = ‘CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB’;
}
$this->addColumn(‘{{%meeting}}’,’identifier’,Schema::TYPE_STRING.’ NOT NULL’);
$all = Meeting::find()
->where([‘identifier’=>»])
->all();
foreach ($all as $m) {
$m->identifier = Yii::$app->security->generateRandomString(8);
$m->update();
}
}
|
Вы заметите, что в этой миграции я фактически использую код для создания случайных строк для каждого существующего собрания, то есть Yii::$app->security->generateRandomString(8);
,
Я не часто пишу код в миграции для обновления существующих областей базы данных. В этом случае все работает плавно. В других случаях я использовал модель /frontend/models/Fix.php.
Также в Meeting::beforeSave()
я добавил автоматический код для генерации идентификатора для всех будущих собраний:
1
2
3
4
5
6
7
8
9
|
public function beforeSave($insert)
{
if (parent::beforeSave($insert)) {
if ($insert) {
$this->identifier = Yii::$app->security->generateRandomString(8);
}
}
return true;
}
|
Расширение Yii Routing
Хотя было бы проще всего включить префикс контроллера, такой как /m/username/identity-code
, я хотел, чтобы ссылки были простыми, без дополнительного префикса. Это потребовало расширения Yii Routing .
Если бы я сохранил это в своей собственной модели для префикса и имени пользователя, я мог бы использовать то, о чем писал в « Сладких поведениях Yii2 и Построении стартапа: геолокация и Google Places» .
Вместо этого я добавил '<username>/<identity:[A-Za-z0-9_-]{8}>' => 'meeting/identity',
которое сопоставляет любое имя пользователя со строкой идентификатора методу actionIdentity()
,
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
‘urlManager’ => [
‘class’ => ‘yii\web\UrlManager’,
‘enablePrettyUrl’ => true,
‘showScriptName’ => false,
//’enableStrictParsing’ => false,
‘rules’ => [
‘place’ => ‘place’,
‘place/yours’ => ‘place/yours’,
‘place/create’ => ‘place/create’,
‘place/create_geo’ => ‘place/create_geo’,
‘place/create_place_google’ => ‘place/create_place_google’,
‘place/view/<id:\d+>’ => ‘place/view’,
‘place/update/<id:\d+>’ => ‘place/update’,
‘place/<slug>’ => ‘place/slug’,
‘<controller:\w+>/<id:\d+>’ => ‘<controller>/view’,
‘<controller:\w+>/<action:\w+>/<id:\d+>’ => ‘<controller>/<action>’,
‘daemon/<action>’ => ‘daemon/<action>’, // incl eight char action
‘site/<action>’ => ‘site/<action>’, // incl eight char action
‘features’ => ‘site/features’,
‘about’ => ‘site/about’,
‘<username>/<identity:[A-Za-z0-9_-]{8}>’ => ‘meeting/identity’,
// note — currently actions with 8 letters and no params will fail
‘<controller:\w+>/<action:\w+>’ => ‘<controller>/<action>’,
],
],
|
С этим я столкнулся с несколькими проблемами. Мне пришлось изменить порядок правил и поместить в статические маршруты любые восемь символьных действий, которые могли бы выглядеть как имя пользователя (вместо контроллера) и метод (вместо ключа идентификации).
Например, https://meetingplanner.io/site/features сопоставлен с сайтом с именем пользователя, имеющим защищенный идентификатор собрания «функций» вместо новой классной таблицы функций Meeting Planner.
Но как только я сориентировался на проблемы, все заработало нормально.
Метод идентификации контроллера собрания
Затем я создал actionIdentity()
в MeetingController:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public function actionIdentity()
{
// fetch path
list($username,$identifier) = explode(«/»,Yii::$app->request->getPathInfo());
// verify the meeting identifier
$m = Meeting::find()
->where([‘identifier’=>$identifier])
->one();
if (is_null($m) || ($m->owner->username != $username)) {
// access failure
return $this->redirect([‘site/authfailure’]);
}
// identifier is authentic
if (Yii::$app->user->isGuest) {
// redir to Participant join form
return $this->redirect([‘/participant/join’,’meeting_id’=>$m->id,’identifier’=>$identifier]);
} else {
$user_id = Yii::$app->user->getId();
if (!Meeting::isAttendee($m->id,$user_id)) {
// if not an attendee — add them as a participant
Participant::add($m->id,$user_id,$m->owner_id);
}
return $this->actionView($m->id);
}
|
Во-первых, он проверяет, что имя пользователя и личность соответствуют существующему пользователю и существующему собранию. Если нет, мы отправляем их в authfailure
.
Если пользователь уже вошел в систему, мы автоматически добавим его в качестве участника на эту страницу и перенаправим на страницу просмотра собрания.
Если это не так, мы отправляем их контроллеру Участника, присоединяющемуся к действию для адресации входа или регистрации.
Участник, желающий присоединиться к собранию
Например, допустим, я получил следующее электронное приглашение от друга:
https://meetingplanner.io/tomeMcFarline/JzRq1a42
. Мне будет показана эта страница:
Если пользователь желает зарегистрироваться в Meeting Planner через социальную сеть, мы сможем подтвердить его адрес электронной почты.
Поэтому я установил URL возврата Yii (страницу, на которую перенаправляется пользователь после успешной регистрации или входа в систему), которая будет возвращать их на страницу просмотра собрания после проверки подлинности.
1
2
|
// set return Url
Yii::$app->user->setReturnUrl($m->getSharingUrl());
|
В большинстве случаев социальная аутентификация, вход в систему и / или регистрация осуществляются с помощью кода, который я описал в разделе «Создание стартапа: упрощение Onramp с помощью OAuth» .
Если участник новый, он сообщит свое имя, фамилию и адрес электронной почты. Мы добавим их на собрание в качестве непроверенного участника, аналогично тому, что мы делаем, когда пользователь приглашает кого-то, добавляя новый адрес электронной почты для собрания.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
$model = new Participant;
$model->meeting_id = $meeting_id;
…
$model->invited_by = $m->owner_id;
$model->status = Participant::STATUS_DEFAULT;
if (!$validationError && $model->validate()) {
$model->participant_id = User::addUserFromEmail($model->email);
$model->save();
// look up email to see if they exist
Meeting::displayNotificationHint($meeting_id);
$user = User::findOne($model->participant_id);
Yii::$app->user->login($user);
return $this->redirect([‘/meeting/view’, ‘id’ => $meeting_id]);
}
|
Вы задаетесь вопросом прямо сейчас, эй Джефф, что с ...
сегодня? Это просто, правда? Мы просто добавляем нового пользователя на встречу.
Кодируя это, я понял, что создаю огромную дыру в конфиденциальности.
Пример забавной дыры в безопасности
Допустим, однажды Тому Макфарлину нечего делать, и он решает связываться со мной (и Богом). Он создаст новую встречу, и, зная, что Далай-лама является постоянным пользователем Планировщика собраний (из-за всех его духовных встреч), Макфарлин добавит его на свою встречу, используя электронную почту [email protected].
Затем он возьмет свой модный безопасный ярлык URL. За мной?
Затем Макфарлин откроет другой браузер и откроет свой безопасный URL-адрес ярлыка и сделает вид, что он Далай-лама, который только что получил другое приглашение от Тома по электронной почте, то есть он попытается присоединиться к собственной встрече, как будто он Далай-лама. то есть Далай, Лама, [email protected].
Первоначально я предполагал, что не было никакой вероятности, что кто-то когда-нибудь угадает безопасный URL, поэтому, если это произойдет, я просто позволю человеку войти таким образом.
Но это дало бы опасный доступ Макфарлина к учетной записи Далай-ламы (отчасти потому, что я еще не создал режим ограниченного доступа для пользователей, заходящих по URL-адресам, чтобы они могли видеть только одно собрание, пока они не войдут в систему).
Да, мой исходный код работал таким образом. А потом мне позвонили с небес и указали на это.
Что если Макфарлин пригласил Салли и Салли переправили безопасный URL Биллу Гейтсу? Добавив Билла Гейтса вручную на собрание первым, Макфарлин смог получить доступ ко всем встречам Гейтса с помощью этого трюка.
Новый код требует, чтобы участник, использующий безопасный URL-адрес, который уже был случайно добавлен в собрание, мог войти в систему вручную. Вот код ...
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
if ($model->load(Yii::$app->request->post())) {
// asking does the person joining already exist in User table
// might have been added to invitation by organizer or might already be a registered user
$person = User::find()->where([’email’=>$model->email])->one();
if (!is_null($person)) {
// user email already exists
// improve their profile
$postedVars = Yii::$app->request->post();
if (!empty($postedVars[‘Participant’][‘firstname’])) {
$model->firstname = $postedVars[‘Participant’][‘firstname’];
}
if (!empty($postedVars[‘Participant’][‘lastname’])) {
$model->lastname = $postedVars[‘Participant’][‘lastname’];
}
UserProfile::improve($person->id,$model->firstname,$model->lastname);
// are they an attendee
if (Meeting::isAttendee($model->meeting_id,$person->id)) {
/*
// to do note — this has to be changed to restricted access mode or removed
$identity = $person->findIdentity($person->id);
Yii::$app->user->login($identity);
// to do — update user profile with first and last name
$this->redirect([‘meeting/view’,’id’=>$model->meeting_id]);
} else {
*/
// caution — don’t turn off this requirement
// a person could add a celebrity to a meeting by using their email with any meeting code and login with their account
Yii::$app->getSession()->setFlash(‘warning’, Yii::t(‘frontend’,’Since you have an account already, please login below.’));
return $this->redirect([‘/site/login’]);
}
}
|
Я исправлю это снова, как только создам режим ограниченного доступа для одной встречи.
Я бы не подумал об этом, если бы не знал, насколько коварен Макфарлин . Уф. Еще одна королева спасена от отравления.
Вероятно, мои долгие выходные на природе помогли мне больше познакомиться с небесами.
Что в трубопроводе?
Надеюсь, вам понравился этот эпизод по созданию безопасных URL-адресов для приглашения людей на встречи. Я полагаю, что он может быть многократно использован для других сценариев в ваших собственных сервисах.
Вы также можете, вероятно, сказать, что я либо наслаждаюсь потенциальным Планировщиком собраний, либо, возможно, слишком долго работал.
В конечном счете, создание безопасных URL-приглашений также открывает возможность предложить пользователям страницу общего планирования. Например, я могу поделиться своим общедоступным URL-адресом Планировщика собраний с друзьями и, скажем, просто запланировать меня на https://meetingplanner.io/username
.
В будущем я мог бы даже расширить Планировщик собраний, чтобы предлагать подписчикам функции подписки на встречи на их общедоступной странице Планировщика собраний. Однако у меня есть другие более интересные идеи. Эта территория хорошо защищена другими бизнес-ориентированными компаниями.
Поездка на волне домой
Если вы еще этого не сделали, запланируйте свою первую встречу с Meeting Planner прямо сейчас! Попробуйте поделиться кратким URL-адресом вашей встречи и держите его в секрете от нашего редактора Тома Макфарлина.
Вы также можете обратиться ко мне @reifman . Я всегда открыт для новых идей и тематических предложений для будущих уроков. Или попробуйте нашу службу поддержки и откройте отчет об ошибке или запрос на добавление функции.
Учебное пособие по краудфандингу также находится в разработке, поэтому, пожалуйста, следуйте нашей странице WeFunder Meeting Planner .
Следите за всеми этими и другими учебниками, ознакомившись с серией « Построение стартапа на PHP» .