Notatki z praktyki

Artykuły

Strona, na której dzielę się zdobytą wiedzą z rzeczywistych projektów.

O czym piszę?

Znajdziesz tutaj kilka ciekawych tekstów na tematy, które opracowałem i uważam, że mogą przydać się w praktyce.

Jak zrobić logowanie przez Facebook w aplikacji .NET MAUI z wykorzystaniem własnego hostingu z PHP

Implementacja logowania przez Facebook w aplikacjach .NET MAUI może sprawiać problemy, szczególnie na iOS. Rozwiązaniem jest OAuth 2.0 Authorization Code Flow, WebAuthenticator oraz własny serwer pośredniczący w PHP.

Wstęp

Wdrożenie logowania przez Facebook w aplikacji mobilnej opartej na .NET MAUI może być wyzwaniem, zwłaszcza gdy chcemy uniknąć instalowania natywnego SDK Facebooka. Nuget z Xamarin.Facebook.iOS i Xamarin.Facebook.Android jest przestarzały, a oficjalne wsparcie dla MAUI wciąż jest ograniczone. Problem ten jest szczególnie widoczny na platformie iOS, gdzie konfiguracja URL Schemes i obsługa callbacków jest bardziej restrykcyjna niż na Androidzie.

Alternatywnym rozwiązaniem jest wykorzystanie mechanizmu OAuth 2.0 Authorization Code Flow wraz z klasą WebAuthenticator dostępną w .NET MAUI oraz własnym serwerem pośredniczącym, czyli Bridge PHP. Serwer może działać na dowolnym hostingu obsługującym PHP, a jego zadaniem jest odbieranie kodu autoryzacyjnego od Facebooka i przekazywanie go do aplikacji mobilnej przez niestandardowy schemat URI.

Takie rozwiązanie pozwala:

  • korzystać z jednego kodu dla Androida i iOS,
  • nie walczyć z Facebook SDK,
  • zachować pełną kontrolę nad procesem logowania,
  • integrować Facebook Login z istniejącym backendem.

Architektura rozwiązania

Przepływ logowania wygląda następująco:


  MAUI App
    |
    | AuthenticateAsync()
    v
Facebook OAuth
    |
    | redirect_uri=https://twojadomena.pl/fb-bridge
    v
fb-bridge.php
    |
    | 302 Redirect
    v
appname://auth?code=...
    |
    v
WebAuthenticator.Callback()
    |
    v
MAUI App
    |
    v
Facebook Graph API
    |
    v
Backend API
              

Dlaczego nie można użyć appname://auth bezpośrednio?

W praktyce Facebook często nie pozwala dodać własnego schematu URI, takiego jak appname://auth, do listy Valid OAuth Redirect URIs w panelu Meta Developers.

W takiej sytuacji należy zastosować pośredni adres HTTPS, na przykład https://twojadomena.pl/fb-bridge, który Facebook zaakceptuje jako poprawny Redirect URI. Następnie serwer wykonuje przekierowanie do aplikacji mobilnej.

Konfiguracja Facebook Developers

W panelu aplikacji Facebook, w sekcji Facebook Login -> Settings, należy dodać https://twojadomena.pl/fb-bridge do listy Valid OAuth Redirect URIs.

Konfiguracja Apache

W katalogu hostingu appname adres /fb-bridge jest kierowany na skrypt PHP przez .htaccess. Ten sam plik wymusza HTTPS oraz ustawia poprawny typ odpowiedzi dla pliku apple-app-site-association.

RewriteEngine On
RewriteCond %{ENV:HTTPS} !on
RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

RewriteRule ^fb-bridge/?$ fb-bridge.php [L,QSA]

<Files "apple-app-site-association">
  ForceType application/json
  Header set Content-Type "application/json"
</Files>

Dzięki temu adres https://twojadomena.pl/fb-bridge będzie obsługiwany przez skrypt PHP, a plik AASA będzie zwracany jako JSON, czego wymaga iOS.

Minimalny układ plików na hostingu
twojadomena.pl/
├── .htaccess
├── fb-bridge.php
├── fb-bridge.html
└── .well-known/
    ├── apple-app-site-association
    └── assetlinks.json

Implementacja fb-bridge.php

Plik odbiera odpowiedź OAuth od Facebooka i przekazuje ją do aplikacji. Warto obsłużyć nie tylko code i state, ale także błędy zwracane przez Facebooka, na przykład error i error_description.

<?php
// fb-bridge.php
// Cel: odebrać redirect z Facebooka (?code=...&state=... albo ?error=...)
// i natychmiast wykonać 302 do custom schematu: appname://auth?...

// Nigdy nic nie wypisuj przed nagłówkami:
ob_start();

// Zabezpieczenia i nagłówki anty-cache przeglądarki/proxy:
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');

// Odbierz parametry z query (code flow)
$code  = isset($_GET['code'])  ? $_GET['code']  : null;
$state = isset($_GET['state']) ? $_GET['state'] : null;

// Jeśli OAuth zwrócił błąd:
$error             = isset($_GET['error'])             ? $_GET['error']             : null;
$error_description = isset($_GET['error_description']) ? $_GET['error_description'] : null;

// Zbuduj URL do aplikacji (custom scheme)
$scheme = 'appname://auth';
$params = [];

if (!empty($code))  { $params['code']  = $code;  }
if (!empty($state)) { $params['state'] = $state; }

if (!empty($error)) {
    $params['error'] = $error;
    if (!empty($error_description)) {
        $params['error_description'] = $error_description;
    }
}

if (empty($params)) {
    http_response_code(200);
    echo "appname fb-bridge: brak parametrów do przekazania.";
    exit;
}

$query  = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$target = $scheme . '?' . $query;

header('Location: ' . $target, true, 302);
exit;

Po wejściu na https://twojadomena.pl/fb-bridge?code=123 użytkownik zostanie przekierowany do appname://auth?code=123. Jeżeli Facebook zwróci błąd, aplikacja dostanie go w callbacku, zamiast czekać bez końca na wynik.

Fallback HTML dla Facebooka

W katalogu musi istnieć także fb-bridge.html, który przekierowuje do aplikacji przez JavaScript. To wariant do weryfikacji dla Facebooka, który nie uruchamia PHP.

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>Redirecting...</title></head>
<body>
<script>
  const q = new URLSearchParams(location.search);
  const code = q.get('code') || '';
  const state = q.get('state') || '';

  const target = `appname://auth?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`;
  location.replace(target);
</script>
</body>
</html>

Przy konfiguracji iOS z Universal Links ten sam adres /fb-bridge pełni dodatkową rolę: iOS może przejąć link HTTPS i przekazać go bezpośrednio do aplikacji. Dlatego dla iOS potrzebna jest konfiguracja Associated Domains oraz plik apple-app-site-association opisany niżej.

Konfiguracja MAUI

AndroidManifest.xml
<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data
        android:scheme="appname"
        android:host="auth" />
</intent-filter>
Info.plist
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>appname</string>
      <string>fb925001276310234</string>
      <string>pl.twojadomena.appname</string>
    </array>
  </dict>
</array>
Associated Domains i apple-app-site-association na iOS

Na iOS sama obsługa niestandardowego schematu URI może nie wystarczyć, jeżeli callback ma wracać przez adres HTTPS używany jako redirect_uri. W takim wariancie trzeba powiązać domenę z aplikacją przez Universal Links.

Po stronie aplikacji iOS należy dodać Associated Domains dla domeny hostującej bridge:

<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:twojadomena.pl</string>
</array>

Po stronie hostingu trzeba wystawić plik .well-known/apple-app-site-association, dostępny pod adresem https://twojadomena.pl/.well-known/apple-app-site-association. Plik nie powinien mieć rozszerzenia .json, nie powinien być zwracany przez przekierowanie i powinien być serwowany jako JSON.

Wartość appID to połączenie Apple Team ID oraz Bundle ID. W wygenerowanym pliku Entitlements.xcent można ją znaleźć pod kluczem application-identifier. Dla appname jest to:

W28L83ZB4A.pl.twojadomena.appname

Docelowy plik .well-known/apple-app-site-association powinien zawierać appID oraz ścieżki obsługiwane przez aplikację:

{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "W28L83ZB4A.pl.twojadomena.appname",
        "paths": ["/fb-bridge", "/fb-bridge/*"]
      }
    ]
  }
}

Tablica apps jest historyczna i zostaje pusta. Ważne są appID oraz ścieżki /fb-bridge i /fb-bridge/*, bo to one pozwalają iOS przekazać URL zwrotny do aplikacji. Jeżeli plik zawiera same paths bez identyfikatora aplikacji, iOS nie ma pełnej informacji, do której aplikacji przypisać link.

assetlinks.json dla Android App Links

Dla samego callbacku appname://auth Android używa niestandardowego schematu URI. Jeżeli jednak domena ma obsługiwać również Android App Links, w katalogu .well-known warto utrzymywać assetlinks.json. W hostingu appname plik wskazuje pakiet pl.twojadomena.appname oraz odciski certyfikatów SHA-256.

[
  {
    "relation": ["delegate_permission/common.handle_all_urls"],
    "target": {
      "namespace": "android_app",
      "package_name": "pl.twojadomena.appname",
      "sha256_cert_fingerprints": [
        "11:CA:1F:A5:1D:43:15:AC:D6:4F:64:51:97:9F:BD:68:AB:EF:31:F5:A6:A5:FD:CA:F5:68:2F:62:D7:C8:A4:C1",
        "A2:C5:52:EB:5E:E9:DA:63:DE:CB:BA:B9:45:3F:C4:FD:24:B8:DA:FD:CD:58:CE:9A:0D:31:A2:53:04:A4:FA:03"
      ]
    }
  }
]

Użyj wcześniej keytool do wygenerowania odcisków certyfikatów:

keytool -list -v -keystore  -alias 

Obsługa callback na iOS

W AppDelegate:

public override bool OpenUrl(
    UIApplication app,
    NSUrl url,
    NSDictionary options)
{
    return WebAuthenticator.Callback(
        new Uri(url.AbsoluteString));
}

Jeżeli iOS zwraca wynik przez Universal Link, trzeba obsłużyć również ContinueUserActivity:

public override bool ContinueUserActivity(
    UIApplication application,
    NSUserActivity userActivity,
    UIApplicationRestorationHandler completionHandler)
{
    if (userActivity.ActivityType == NSUserActivityType.BrowsingWeb &&
        userActivity.WebPageUrl is not null)
    {
        return WebAuthenticator.Callback(
            new Uri(userActivity.WebPageUrl.AbsoluteString));
    }

    return base.ContinueUserActivity(
        application,
        userActivity,
        completionHandler);
}

Rozpoczęcie logowania

const string fbAppId = "925001276310234";
const string facebookRedirectUri = "https://twojadomena.pl/fb-bridge";
const string appCallbackUri = "appname://auth";

var pkce = PkceUtil.CreateS256();

Budowanie URL:

var authUri = new Uri(
    "https://www.facebook.com/v19.0/dialog/oauth" +
    $"?client_id={fbAppId}" +
    $"&redirect_uri={Uri.EscapeDataString(facebookRedirectUri)}" +
    $"&response_type=code" +
    $"&code_challenge={pkce.CodeChallenge}" +
    $"&code_challenge_method=S256" +
    $"&scope=public_profile,email");

Uruchomienie logowania:

var callbackUri =
    DeviceInfo.Platform == DevicePlatform.iOS
        ? new Uri(facebookRedirectUri)
        : new Uri(appCallbackUri);

var result =
    await WebAuthenticator.Default.AuthenticateAsync(
        authUri,
        callbackUri);

Po poprawnym logowaniu:

var authCode = result.Properties["code"];

Wymiana code na access_token

Facebook wymaga wykonania dodatkowego wywołania:

var tokenUrl =
    "https://graph.facebook.com/v19.0/oauth/access_token" +
    $"?client_id={fbAppId}" +
    $"&redirect_uri={Uri.EscapeDataString(facebookRedirectUri)}" +
    $"&code={Uri.EscapeDataString(authCode)}" +
    $"&code_verifier={Uri.EscapeDataString(pkce.CodeVerifier)}";

Pobranie tokena:

var tokenJson =
    await http.GetStringAsync(tokenUrl);

Pobranie danych użytkownika

var meJson =
    await http.GetStringAsync(
        $"https://graph.facebook.com/me?fields=id,email&access_token={accessToken}");

Przykładowa odpowiedź:

{
  "id": "1307126283021094",
  "email": "user@example.com"
}

Logowanie do własnego backendu

Po uzyskaniu facebookUserId i accessToken można przekazać je do własnego API:

ProfileManagerInstance.LoginBySocialConnect(
    userId,
    accessToken,
    SocialConnectRequestModel.SocialConnect.Facebook);

Backend powinien:

  1. Zweryfikować token przez Graph API.
  2. Pobrać dane użytkownika.
  3. Utworzyć konto lub odnaleźć istniejące.
  4. Zalogować użytkownika.
  5. Wysłać komunikat powitalny do aplikacji.

Typowe problemy

Facebook zwraca: Brak adresu URL przekierowania w parametrach

Przyczyną jest zwykle redirect_uri, które nie jest zgodne z wpisem w Valid OAuth Redirect URIs.

AuthenticateAsync nigdy nie wraca

Najczęstsze przyczyny:

  • brak konfiguracji URL Scheme,
  • brak OpenUrl w AppDelegate,
  • brak Associated Domains albo pliku .well-known/apple-app-site-association dla callbacku HTTPS na iOS,
  • brak obsługi ContinueUserActivity dla Universal Links na iOS,
  • bridge nie wykonuje przekierowania,
  • redirect trafia na stronę HTML zamiast do aplikacji.
Android działa, iOS nie

Najczęściej oznacza to brak WebAuthenticator.Callback(...) w AppDelegate, brak obsługi Universal Links albo niepoprawny appID w pliku apple-app-site-association.

Zalety rozwiązania

  • brak Facebook SDK,
  • wspólny kod Android/iOS,
  • prostsza migracja Xamarin -> MAUI,
  • możliwość integracji z własnym backendem,
  • pełna kontrola nad procesem OAuth.

Podsumowanie

Połączenie .NET MAUI, WebAuthenticator oraz własnego skryptu fb-bridge.php pozwala wdrożyć logowanie Facebook bez używania natywnego SDK Facebooka. Rozwiązanie działa zarówno na Androidzie, jak i na iOS, a jednocześnie pozwala zachować pełną kontrolę nad wymianą tokenów oraz integracją z własnym systemem użytkowników.

W przypadku aplikacji MAUI taki model jest szczególnie wygodny, ponieważ backend PHP już weryfikuje token Facebooka przez Graph API i integruje logowanie z istniejącym systemem użytkowników oraz komunikacją ProtoBuf. Dzięki temu Facebook pełni wyłącznie rolę dostawcy tożsamości, a cała logika autoryzacji pozostaje po stronie własnego serwera.

n8n w języku polskim. Prosty hack z Tampermonkey

Platforma n8n to potężne narzędzie do automatyzacji, ale oficjalnie wspiera wyłącznie język angielski. Jest jednak prosty sposób, aby lokalnie spolszczyć interfejs bez przebudowywania Dockera.

Gdy hostujemy n8n na własnym serwerze VPS, na przykład przez Docker Compose, szybko orientujemy się, że wstrzyknięcie plików językowych .json do kontenera niewiele daje. Dlaczego? Ponieważ interfejs przeglądarkowy jest kompilowany przed publikacją, a angielskie teksty są na stałe zaszyte w plikach JavaScript.

Rozwiązania są dwa: uciążliwe przebudowywanie własnego obrazu Dockera po każdej aktualizacji n8n albo sprytny hack w przeglądarce. Poniżej pokazuję drugi wariant z użyciem darmowej wtyczki Tampermonkey.

Czym jest Tampermonkey i jak nam pomoże?

Tampermonkey to popularne rozszerzenie do przeglądarek Chrome, Firefox, Edge i Safari, które pozwala uruchamiać niestandardowe skrypty JavaScript, tak zwane userscripty, na określonych stronach internetowych.

Zamiast modyfikować pliki na serwerze VPS, napiszemy krótki skrypt, który w locie będzie obserwował interfejs n8n w przeglądarce i podmieniał angielskie frazy, takie jak Workflows czy Credentials, na ich polskie odpowiedniki: Workflowy i Poświadczenia.

Instrukcja krok po kroku

Krok 1: Zainstaluj wtyczkę

Pobierz i zainstaluj rozszerzenie Tampermonkey z oficjalnego sklepu z dodatkami Twojej przeglądarki.

Krok 2: Utwórz nowy skrypt

Kliknij ikonę Tampermonkey na pasku przeglądarki i wybierz Utwórz nowy skrypt. Wyczyść domyślną zawartość edytora i wklej poniższy kod:

Ładowanie kodu...
Krok 3: Dopasuj adres swojego serwera

W linii // @match https://twoja-domena-lub-ip.pl/* zmień adres na ten, pod którym logujesz się do swojego n8n, na przykład https://n8n.moj-vps.pl/* albo http://123.45.67.89:5678/*. Gwiazdka * na końcu jest obowiązkowa.

Krok 4: Zapisz i gotowe

Wciśnij Ctrl + S lub wybierz Plik -> Zapisz. Teraz wystarczy odświeżyć kartę z n8n. Od tej pory zdefiniowane w słowniku frazy będą automatycznie zamieniane na język polski.

Zalety i wady tego rozwiązania

Dlaczego warto?

  • Pełna odporność na aktualizacje: modyfikujemy widok w przeglądarce, a nie kod źródłowy n8n, więc tłumaczenie przetrwa aktualizacje kontenerów Docker.
  • Bezpieczeństwo: skrypt działa lokalnie w przeglądarce i nie ma dostępu do bazy danych ani nie obciąża serwera.
  • Dowolność: jeśli nie podoba Ci się tłumaczenie konkretnego przycisku, zmieniasz je w słowniku Tampermonkey w kilka sekund.

O czym trzeba pamiętać?

  • Rozwiązanie lokalne: tłumaczenie działa tylko w przeglądarce, w której masz zainstalowaną wtyczkę.
  • Efekt mignięcia: na wolniejszych komputerach możesz przez moment zobaczyć angielskie słowo, zanim zostanie przetłumaczone.

Mimo tych ograniczeń jest to szybki i mało inwazyjny sposób na to, aby codzienna praca z n8n na własnym VPS odbywała się w języku polskim.

AI nie zaczyna się od modelu. Zaczyna się od procesu.

Najczęstszy błąd przy wdrażaniu sztucznej inteligencji polega na szukaniu narzędzia, zanim firma nazwie problem, dane i odpowiedzialność za wynik.

W rozmowach o AI łatwo zacząć od listy modeli, licencji i integracji. To naturalne, bo technologia jest widoczna i szybko daje efekt demonstracyjny. W praktyce jednak największa wartość pojawia się dopiero wtedy, gdy narzędzie zostaje osadzone w konkretnym procesie: obsłudze klienta, analizie dokumentów, wsparciu sprzedaży, raportowaniu albo kontroli jakości danych.

Pierwszym krokiem powinno być rozpisanie pracy tak, jak dzieje się ona dzisiaj. Kto inicjuje zadanie? Jakie informacje są potrzebne? Gdzie powstają opóźnienia? Które decyzje wymagają doświadczenia, a które są powtarzalne? Dopiero taka mapa pokazuje, czy AI ma podpowiadać, automatyzować, streszczać, klasyfikować, czy może jedynie porządkować dane przed decyzją człowieka.

Drugim elementem jest jakość danych. Model językowy może przyspieszyć analizę, ale nie naprawi chaotycznego źródła informacji bez dodatkowych reguł, walidacji i kontroli. Dlatego dobre wdrożenie AI często obejmuje mniej spektakularne prace: uporządkowanie dokumentów, definicję słowników, integrację systemów i ustalenie, kto zatwierdza wynik.

Trzecim elementem jest mierzenie efektu. Warto wybrać dwa lub trzy wskaźniki, które są zrozumiałe biznesowo: czas obsługi sprawy, liczba błędów, koszt przygotowania raportu, czas wdrożenia nowego pracownika albo liczba decyzji wymagających eskalacji. Dzięki temu AI przestaje być eksperymentem, a staje się elementem operacyjnym.

Dobre pytanie nie brzmi więc: jaki model wdrożyć? Lepsze pytanie brzmi: który proces jest wystarczająco częsty, kosztowny i mierzalny, aby automatyzacja mogła realnie zmienić wynik firmy? Od tej odpowiedzi powinien zaczynać się projekt.

Dobry plan nie jest listą życzeń. Jest narzędziem decyzji.

Roadmapa pomaga zespołowi tylko wtedy, gdy pokazuje priorytety, zależności i koszt rezygnacji z alternatywnych kierunków rozwoju.

W wielu organizacjach roadmapa szybko zamienia się w katalog oczekiwanych funkcji. Każdy dział dopisuje własne potrzeby, terminy zaczynają wyglądać jak zobowiązania, a zespół technologiczny dostaje kolejkę prac bez jasnej odpowiedzi, które cele biznesowe są najważniejsze. Taki dokument nie porządkuje decyzji. On jedynie zapisuje napięcia.

Skuteczna roadmapa powinna zaczynać się od problemów, nie od rozwiązań. Zamiast wpisu „dodać moduł raportów” lepiej nazwać rezultat: „skrócić przygotowanie miesięcznego raportu z dwóch dni do dwóch godzin”. Taki zapis pozwala rozważyć kilka ścieżek: automatyzację eksportu, zmianę modelu danych, gotowy panel BI albo prosty proces zatwierdzania. Zespół odzyskuje możliwość zaproponowania rozwiązania, a biznes może ocenić efekt.

Drugą funkcją roadmapy jest pokazanie zależności. Niektóre prace wyglądają mało atrakcyjnie, ale otwierają drogę do wielu kolejnych zmian: uporządkowanie uprawnień, przebudowa integracji, ujednolicenie danych klienta albo poprawa testów automatycznych. Jeżeli te fundamenty nie są widoczne, łatwo przegrają z funkcjami, które są bardziej efektowne w prezentacji.

Trzecim zadaniem roadmapy jest ochrona koncentracji. Każde „tak” dla nowej funkcji oznacza „nie teraz” dla innej. Dlatego warto przypisać priorytety do celów, a nie do osób zgłaszających potrzeby. Pomaga prosta klasyfikacja: wpływ na przychód, ryzyko operacyjne, koszt utrzymania, oczekiwania klientów i zgodność z długofalową architekturą.

Roadmapa nie musi przewidywać przyszłości z dokładnością do tygodnia. Powinna natomiast jasno mówić, dlaczego firma wybiera tę kolejność prac, jakie założenia stoją za decyzją i po czym pozna, że warto zmienić plan. Wtedy staje się żywym narzędziem zarządzania, a nie dekoracją do spotkań statusowych.