{
    "version": "https:\/\/jsonfeed.org\/version\/1.1",
    "title": "Робота",
    "_rss_description": "Всім привіт! Я — Богдан, фулстек розробник з Києва. Пишу в основному про програмування та штуки які вивчаю. Час від часу публікую підбірки фоток, зроблених на плівку, розказую про подорожі та цікаві речі, що оточують мене.",
    "_rss_language": "uk",
    "_itunes_email": "",
    "_itunes_categories_xml": "",
    "_itunes_image": "",
    "_itunes_explicit": "",
    "home_page_url": "https:\/\/stefaniuk.website\/tags\/my-work\/",
    "feed_url": "https:\/\/stefaniuk.website\/tags\/my-work\/json\/",
    "icon": "https:\/\/stefaniuk.website\/pictures\/userpic\/userpic@2x.jpg?1565716580",
    "authors": [
        {
            "name": "Bohdan Stefaniuk",
            "url": "https:\/\/stefaniuk.website\/",
            "avatar": "https:\/\/stefaniuk.website\/pictures\/userpic\/userpic@2x.jpg?1565716580"
        }
    ],
    "items": [
        {
            "id": "262",
            "url": "https:\/\/stefaniuk.website\/all\/app-manuals-by-wordpress-and-identityserver\/",
            "title": "Онлайн документация на базе WordPress и IdentityServer",
            "content_html": "<p>Как-то раз на работе появилась задача по созданию онлайн документации для нашего приложения. Документация должна быть не технической, а в виде мануалов для пользователей.<\/p>\n<p>Одним из главных требований была авторизация пользователей с помощью текущего логина и пароля от основного приложения. Доступ к материалам должен определяться на основании текущих ролей пользователя.<\/p>\n<p>Изначально хотели использовать готовое решение в виде SaaS сервиса с которым можно интегрироваться и спокойно пользоваться. Идея самим все написать с нуля хоть и возникала, но вернуться к ней решили только в крайнем случае.<\/p>\n<p>Первым кандидатом был GitBook. У него приятный интерфейс, много опций по редактированию и оформлению текстов. Необходимый нам функционал также был, но доступен только в Enterprise версии. Цена оказалась слишком большой, пришлось искать дальше.<\/p>\n<p>Подобных сервисов и так не очень много, а с необходимыми нам фичами так вообще почти нету. В какой-то момент я даже решил попробовать что-то собрать на основе статического генератора сайтов, но получилось слишком монструозно.<\/p>\n<p><b>Wordpress<\/b><\/p>\n<p>Оставался только один вариант — написать все самим, чего очень не хотелось делать. В какой-то момент я чисто случайно вспомнил про Wordpress. Не то что бы я им раньше пользовался, но много слышал о нем.<\/p>\n<p>В итоге решили делать мануалы на базе Wordpress. Вообще, Wordpress это такая штука, с которой при должном умении можно слепить все что угодно. В нашем случае даже код не пришлось писать (почти).<\/p>\n<p>За основу взяли готовую тему и немного допилили под наши нужды с помощью CSS. Правда в одном месте пришлось немного изменить логику отображения с помощью PHP, но это была достаточно тривиальная задача.<\/p>\n<p>Весь функционал реализовали с помощью пачки плагинов. Главным среди них стал OAuth Single Sign On by miniOrange. Он позволяет логиниться в вордпесс с помощью сторонних сервисов (Google, Facebook и т. д.). Бесплатная версия покрывает почти все кейсы. На же пришлось покупать лицензию, потому что нужен был мапинг наших ролей на роли вордпреса.<\/p>\n<p>За работу с пользователями в нашем приложении отвечает IdentityServer. Сделали отдельную страницу логина для SSO чтобы получить нужную куку. Также включили флаг AlwaysIncludeUserClaimsInIdToken в настройках клиента, чтобы клеймы ролей были в ID токене и их смог увидеть Wordpress.<\/p>\n<p><b>Хостинг<\/b><\/p>\n<p>С хостингом тоже все оказалось достаточно просто. Изначально хотели использовать AWS Lightsail, но основная проблема мы в отсутствии возможности перемещать снапшоты диска между аккаунтами. В итоге развернули все на EC2 с помощью готового бесплатного решения из AWS Marketplace.<\/p>\n<p><b>Итого<\/b><\/p>\n<p>Эта задача очень показательна, она позволяет понять насколько готовые решения позволяют сэкономить время и деньги. Если писать такое приложение с нуля, то это может занять месяц работы, а также время на тестирование и отлов типичных ошибок. Ну и пользователей нужно было бы учить как всем этим пользоваться.<\/p>\n<p>В нашем случае настройка Wordpress заняла приблезительно неделю не очень активной работы. Также популярность Wordpress позволяет легко нанять человека, который сможет спокойно с ним работать.<\/p>\n<p>Ну и стоимость порадовала. Все это обошлось в ~570$, в то время как типичный SaaS сервис берез 5$ в месяц за одного пользователя. Хостинг тоже вышел не очень дорогим (~10-15$ в месяц).<\/p>\n",
            "date_published": "2021-12-22T03:55:03-04:00",
            "date_modified": "2023-06-02T20:04:57-04:00",
            "tags": [
                "інструменти",
                "дешево і сердито",
                "робота"
            ],
            "_date_published_rfc2822": "Wed, 22 Dec 2021 03:55:03 -0400",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "262",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": []
            }
        },
        {
            "id": "171",
            "url": "https:\/\/stefaniuk.website\/all\/create-workplace-messenger-bot\/",
            "title": "Как я делал бота для Facebook workplace",
            "content_html": "<p>Дали мне задачу: написать бота для мессенджера workplace, с помощью которого можно получать уведомления из нашей CRM и управлять разными вещами. Расскажу о разных интересных вещах с которыми я столкнулся. Бота писал с помощью ASP.NET Core Web API.<\/p>\n<h2>Вебхуки<\/h2>\n<p>Для того чтобы бот мог обрабатывать запросы и сообщения от Facebook нам надо настроить вебхуки. Webhook — механизм оповещения системы о событиях. Для того чтобы Facebook принял наш хук, он должен обрабатывать как GET так и POST запросы.<\/p>\n<p class=\"note\"><a href=\"https:\/\/developers.facebook.com\/docs\/messenger-platform\/getting-started\/webhook-setup\">Подробнее о Webhook в официальной документации<\/a><\/p>\n<p>GET запрос служит для валидации работы нашего эндпоинта. POST принимает данные связанные с активностью пользователя, будь то нажатие на кнопки или другая активность.<\/p>\n<p class=\"cut-button\">Показать код метода-обработчика GET запроса<\/p>\n<div class=\"cut-content\"><pre class=\"e2-text-code\"><code class=\"\">public IActionResult Receive(\u2028[FromQuery(Name = &quot;hub.mode&quot;)] string mode,\u2028\r\n    [FromQuery(Name = &quot;hub.challenge&quot;)] string challenge,\u2028\r\n    [FromQuery(Name = &quot;hub.verify_token&quot;)] string verifyToken)\r\n{\u2028\r\n   if (string.IsNullOrEmpty(verifyToken)) {\u2028\r\n       return Unauthorized();\u2028\r\n    }\u2028\u2028\r\n    if (verifyToken.Equals(FacebookEnvironment.FacebookVToken)) {\u2028\r\n        return Ok(challenge);\u2028\r\n    }\u2028\r\n    return Unauthorized();\u2028\r\n}<\/code><\/pre><p>В качестве verify_token используется токен, который мы указали при регистрации нашего хука.<\/p>\n<\/div><p class=\"cut-button\">Показать код метода-обработчика POST запроса<\/p>\n<div class=\"cut-content\"><pre class=\"e2-text-code\"><code class=\"\">public async Task&lt;IActionResult&gt; Receive([FromBody]FbResponse response = null)\r\n{\r\n    if (response is null) {\r\n        return BadRequest();\r\n    }\r\n\r\n    if (response.Object != &quot;page&quot;) {\r\n        return Ok();\r\n    }\r\n\r\n    foreach (var entry in response.Entries) {\r\n        foreach (var message in entry.Messaging) {\r\n            await PrepareMessageAsync(message);\r\n        }\r\n    }\r\n    return Ok(&quot;EVENT_RECEIVED&quot;);\r\n}<\/code><\/pre><\/div><h2>Авторизация запросов Facebook<\/h2>\n<p>Для авторизации Facebook использует специальный http заголовок (X-Hub-Signature), в нем он передает некую сигнатуру с помощью которой мы можем авторизовать запрос. Для того чтобы добавить такую функциональность в наш контроллер, добавим фильтр.<\/p>\n<p class=\"cut-button\">Пример кода, для проверки подписи<\/p>\n<div class=\"cut-content\"><pre class=\"e2-text-code\"><code class=\"\">private const string Sha1Prefix = &quot;sha1=&quot;;\r\n\r\npublic static bool Validate(string signature, string contentString) {\r\n    if (!signature.StartsWith(Sha1Prefix, StringComparison.OrdinalIgnoreCase)) {\r\n        return false;\r\n    }\r\n    var secret = Encoding.ASCII.GetBytes(FacebookEnvironment.AppSecret);\r\n    var signatureWithoutPrefix = signature.Substring(Sha1Prefix.Length);\r\n    var content = Encoding.ASCII.GetBytes(contentString);\r\n    return GetIsHashValid(secret, signatureWithoutPrefix, content);\r\n}\r\n\r\nprivate static bool GetIsHashValid(byte[] secret, string signature, byte[] content) {\r\n    using var hmac = new HMACSHA1(secret);\r\n    var hash = hmac.ComputeHash(content);\r\n    var hashString = ToHexString(hash);\r\n    return hashString.Equals(signature);\r\n}\r\n\r\nprivate static string ToHexString(IReadOnlyCollection&lt;byte&gt; bytes)\r\n{\r\n    var builder = new StringBuilder(bytes.Count * 2);\r\n    foreach (var b in bytes)\r\n    {\r\n        builder.AppendFormat(&quot;{0:x2}&quot;, b);\r\n    }\r\n\r\n    return builder.ToString();\r\n}<\/code><\/pre><\/div><h2>Тестирование бота<\/h2>\n<p class=\"note-md\"><a href=\"http:\/\/winitpro.ru\/index.php\/2017\/11\/03\/ustanovka-besplatnogo-ssl-sertifikata-lets-encrypt-na-iis-v-windows-server-2012-r2\/\">Как установить letsencrypt сертификат для IIS<\/a>. Если же вы используете связку в виде ubuntu и nginx вам подойдет <a href=\"https:\/\/www.digitalocean.com\/community\/tutorials\/nginx-let-s-encrypt-ubuntu-18-04-ru\">эта инструкция<\/a>.<\/p>\n<p>Для тестирования нужно развернуть бот на сервере, который смотрит в мир. Также необходимо чтобы у сервера было доменное имя и валидный SSL сертификат. В моем случае, в качестве сервера выступала машина на винде, так как другой внутри нашей сети не было. Как мне показалось захостить приложение написанное на .NET Core намного проще под Ubuntu + nginx нежели под Windows + IIS. В качестве поставщика сертификатов выбрал letsencrypt, так как они предоставляют бесплатный сертификат на 3 месяца, с возможностью дальнейшего обновления.<\/p>\n",
            "date_published": "2019-11-26T10:22:26-04:00",
            "date_modified": "2023-06-02T20:26:31-04:00",
            "tags": [
                "asp.net core",
                "dotnet",
                "програмування",
                "проекти",
                "робота"
            ],
            "_date_published_rfc2822": "Tue, 26 Nov 2019 10:22:26 -0400",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "171",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [
                    "system\/library\/highlight\/highlight.js",
                    "system\/library\/highlight\/highlight.css"
                ],
                "og_images": []
            }
        },
        {
            "id": "178",
            "url": "https:\/\/stefaniuk.website\/all\/terrasoft-epam\/",
            "title": "Terrasoft → Epam",
            "content_html": "<p>Сегодня был мой последний день в Террасофте. Проработал здесь 2,2 года (801 день). Это были забавные времена, было много позитивных вещей и часть негативных. Точно я могу сказать одно, я получил массу полезного опыта, новые знакомства, скиллы. А теперь пришло время идти дальше.<\/p>\n",
            "date_published": "2019-11-22T18:14:27-04:00",
            "date_modified": "2020-04-22T09:14:48-04:00",
            "tags": [
                "робота"
            ],
            "_date_published_rfc2822": "Fri, 22 Nov 2019 18:14:27 -0400",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "178",
            "_e2_data": {
                "is_favourite": false,
                "links_required": [],
                "og_images": []
            }
        },
        {
            "id": "85",
            "url": "https:\/\/stefaniuk.website\/all\/36\/",
            "title": "36",
            "content_html": "<p>Просто советую всем посмотреть. Особенно программистам.<\/p>\n<div class=\"e2-text-video\">\n<iframe src=\"https:\/\/www.youtube.com\/embed\/FxljIvLxUqQ?enablejsapi=1\" allow=\"autoplay\" frameborder=\"0\" allowfullscreen><\/iframe>\n<\/div>\n",
            "date_published": "2019-07-01T04:57:52-04:00",
            "date_modified": "2023-06-02T20:21:45-04:00",
            "tags": [
                "видео",
                "програмування",
                "робота"
            ],
            "image": "https:\/\/stefaniuk.website\/pictures\/remote\/youtube-FxljIvLxUqQ-cover.jpg",
            "_date_published_rfc2822": "Mon, 01 Jul 2019 04:57:52 -0400",
            "_rss_guid_is_permalink": "false",
            "_rss_guid": "85",
            "_e2_data": {
                "is_favourite": true,
                "links_required": [
                    "system\/library\/jquery\/jquery.js",
                    "system\/library\/media-seek\/media-seek.js"
                ],
                "og_images": [
                    "https:\/\/stefaniuk.website\/pictures\/remote\/youtube-FxljIvLxUqQ-cover.jpg"
                ]
            }
        }
    ],
    "_e2_version": 4116,
    "_e2_ua_string": "Aegea 11.2 (v4116)"
}