<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Artykuły on SpaceShaman</title>
    <link>https://spaceshaman.github.io/pl/posts/</link>
    <description>Recent content in Artykuły on SpaceShaman</description>
    <generator>Hugo</generator>
    <language>pl-PL</language>
    <copyright>SpaceShaman</copyright>
    <lastBuildDate>Fri, 21 Aug 2026 08:02:43 +0000</lastBuildDate>
    <atom:link href="https://spaceshaman.github.io/pl/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Dlaczego zastąpiłem krzesło poduszką do medytacji</title>
      <link>https://spaceshaman.github.io/pl/posts/why-i-replaced-my-chair-with-a-meditation-cushion/</link>
      <pubDate>Fri, 21 Aug 2026 08:02:43 +0000</pubDate>
      <guid>https://spaceshaman.github.io/pl/posts/why-i-replaced-my-chair-with-a-meditation-cushion/</guid>
      <description>Dwuletni eksperyment polegający na zastąpieniu krzesła biurowego miejscem pracy na podłodze.</description>
      <content:encoded><![CDATA[<h2 id="dlaczego">Dlaczego?</h2>
<p>Od bardzo dawna fascynuje mnie kultura Japonii. Jedną z rzeczy, które zawsze zwracały moją uwagę, jest długowieczność i ogólnie dobry stan zdrowia Japończyków.</p>
<p>Ciekawym elementem japońskiej kultury jest to, że ludzie spędzają dużo czasu, siedząc na podłodze. Większość z was zapewne widziała w japońskich filmach sceny, w których ludzie siedzą na podłodze wokół niskiego stołu.</p>
<p>Zacząłem się zastanawiać, czy siedzenie na podłodze może być jednym z czynników wpływających na ich zdrowie i długowieczność oraz czy mógłbym wprowadzić ten zwyczaj do własnego życia.</p>
<p>Za każdym razem, gdy siadasz na podłodze i z niej wstajesz, twoje ciało musi wykonać określony ruch. W pewnym sensie przypomina to zrobienie przysiadu, a przysiady są bardzo korzystne dla ciała. Ponadto siedzenie w pozycji podobnej do medytacyjnej zachęca nas do utrzymywania lepszej postawy.</p>
<p>Ponieważ od dawna praktykowałem medytację i miałem już poduszkę do medytacji, postanowiłem przeprowadzić eksperyment: zastąpiłem nią krzesło biurowe, którego używałem do pracy.</p>
<p>Zmodyfikowałem biurko tak, by jego wysokość nadawała się do pracy na siedząco na podłodze, i rozpocząłem eksperyment.</p>
<p><img alt="Miejsce pracy na podłodze z poduszką do medytacji" loading="lazy" src="/images/floor-workspace.jpg"></p>
<h2 id="pierwszy-miesiąc">Pierwszy miesiąc</h2>
<p>Pierwszy miesiąc był znacznie trudniejszy, niż się spodziewałem. Już po kilku dniach byłem bliski rezygnacji. Bolały mnie kolana i plecy, a mimo doświadczenia w medytacji nie czułem się komfortowo, siedząc na podłodze przez tak długi czas.</p>
<p>Szybko stało się jasne, że siedzenie na podłodze przez osiem godzin dziennie to coś zupełnie innego niż trzydziestominutowa medytacja.</p>
<p>Mimo początkowych trudności postanowiłem kontynuować eksperyment i dać organizmowi wystarczająco dużo czasu na przystosowanie się.</p>
<p>Po pierwszym miesiącu ból kolan i pleców zniknął, a ja zacząłem czuć się znacznie wygodniej. Zauważyłem również niewielką poprawę postawy.</p>
<h2 id="dwa-lata-później-nadal-pracuję-na-podłodze">Dwa lata później nadal pracuję na podłodze</h2>
<p>Od rozpoczęcia tego eksperymentu minęły już dwa lata i nie zamierzam wracać do siedzenia na krześle biurowym. Czuję się znacznie lepiej niż wcześniej, moja postawa wyraźnie się poprawiła i nie potrafię już sobie wyobrazić spędzania wielu godzin na wygodnym krześle biurowym — które, o ironio, wcale nie wydaje mi się już wygodne.</p>
<p>Odkryłem również, że siedzenie na podłodze pozwala przyjmować wiele różnych pozycji, co także może mieć pozytywny wpływ na ciało. Spędzanie całego dnia w jednej pozycji jest bardzo niezdrowe.</p>
<p>Pomocna może być tutaj joga. Wiele asan da się dostosować do siedzenia i pracy przy komputerze, dzięki czemu możemy regularnie zmieniać pozycję, w której pracujemy.</p>
<p><strong>Co sądzisz o takim miejscu pracy? Czy rozważyłbyś zastąpienie krzesła stanowiskiem na podłodze? A może masz własny nietypowy sposób pracy przy komputerze, dzięki któremu czujesz się wygodniej albo pozostajesz w ruchu?</strong></p>
<p><em>Artykuł pierwotnie opublikowany po angielsku na <a href="https://coderlegion.com/25062/why-i-replaced-my-chair-with-a-meditation-cushion">CoderLegion</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Rozwój UserHarbor: od niezależnego rdzenia do wykonywalnych kontraktów warstwy danych</title>
      <link>https://spaceshaman.github.io/pl/posts/building-userharbor-framework-agnostic-user-management-for-python/</link>
      <pubDate>Mon, 17 Aug 2026 12:31:04 +0000</pubDate>
      <guid>https://spaceshaman.github.io/pl/posts/building-userharbor-framework-agnostic-user-management-for-python/</guid>
      <description>Jak UserHarbor rozwinął się od niezależnego rdzenia w architekturę opartą na adapterach i wykonywalnym kontrakcie warstwy danych.</description>
      <content:encoded><![CDATA[<p>Zarządzanie użytkownikami jest jednym z tych problemów, które rzadko wydają się na tyle trudne, by poświęcać im wiele uwagi.</p>
<p>Dopóki nie zaimplementujesz go po raz piąty.</p>
<p>Rejestracja, logowanie, sesje, weryfikacja adresu e-mail, resetowanie i zmiana hasła, usuwanie konta, role, uprawnienia — żadna z tych funkcji nie jest szczególnie nietypowa. Niemal każda aplikacja potrzebuje jednak jakiejś ich kombinacji, a implementacja często staje się ściśle powiązana z frameworkiem, ORM-em lub infrastrukturą, których akurat użyto w projekcie.</p>
<p>Właśnie ten problem skłonił mnie do zbudowania <strong>UserHarbor</strong>.</p>
<p>UserHarbor to niezależna od frameworka biblioteka Pythona do zarządzania kontami użytkowników. Jej celem nie jest stworzenie kolejnego frameworka webowego ani kompletnej platformy tożsamości. Zamiast tego udostępnia niewielkie API na poziomie domeny dla typowych operacji na kontach, pozostawiając obsługę HTTP, baz danych i wysyłkę wiadomości e-mail osobnym integracjom.</p>
<p>Odkąd po raz pierwszy napisałem o tym projekcie, coraz ciekawsza staje się dla mnie nie tylko sama warstwa uwierzytelniania, lecz przede wszystkim granica pomiędzy rdzeniem a jego integracjami.</p>
<p><em>Ten artykuł rozwija <a href="/pl/posts/i-built-userharbor-a-framework-agnostic-user-management-library-for-python/">moje pierwotne wprowadzenie do UserHarbor</a> i skupia się na tym, jak zmieniała się architektura wraz z rozwojem projektu.</em></p>
<h2 id="problem-który-chciałem-rozwiązać">Problem, który chciałem rozwiązać</h2>
<p>Wyobraźmy sobie budowę dwóch aplikacji.</p>
<p>Pierwsza korzysta z:</p>
<ul>
<li>FastAPI</li>
<li>SQLAlchemy</li>
<li>PostgreSQL</li>
<li>SMTP</li>
</ul>
<p>Druga korzysta z:</p>
<ul>
<li>Flaska</li>
<li>MongoDB</li>
<li>zewnętrznego API do wysyłania wiadomości e-mail</li>
</ul>
<p>Reguły zarządzania użytkownikami są w większości takie same.</p>
<p>Hasło nadal trzeba zweryfikować i zahaszować. Token weryfikacyjny nadal musi wygasnąć. Tokeny resetowania hasła nadal wymagają ochrony. Sesje trzeba tworzyć i unieważniać. Role i uprawnienia trzeba sprawdzać.</p>
<p>W wielu bibliotekach reguły te są jednak wymieszane z modelami bazy danych, handlerami HTTP albo abstrakcjami właściwymi dla konkretnego frameworka.</p>
<p>Chciałem czegoś przeciwnego.</p>
<p>Rdzeń powinien wiedzieć, <strong>co powinno się wydarzyć</strong>, ale niekoniecznie <strong>w jaki sposób aplikacja przechowuje lub przesyła dane</strong>.</p>
<p>Prowadzi to do architektury wyglądającej mniej więcej tak:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Aplikacja / framework
</span></span><span class="line"><span class="cl">        │
</span></span><span class="line"><span class="cl">        ▼
</span></span><span class="line"><span class="cl">    UserHarbor
</span></span><span class="line"><span class="cl">        │
</span></span><span class="line"><span class="cl">        ├── UserStore
</span></span><span class="line"><span class="cl">        │       └── baza danych / ORM / własny backend
</span></span><span class="line"><span class="cl">        │
</span></span><span class="line"><span class="cl">        └── EmailSender
</span></span><span class="line"><span class="cl">                └── SMTP / API / własny dostawca
</span></span></code></pre></div><p>Rdzeń odpowiada za rejestrację, walidację, haszowanie haseł, generowanie i haszowanie tokenów, obsługę sesji oraz reguły autoryzacji.</p>
<p>Adaptery odpowiadają za infrastrukturę.</p>
<h2 id="niewielkie-api-na-poziomie-domeny">Niewielkie API na poziomie domeny</h2>
<p>UserHarbor obsługuje obecnie typowy cykl życia konta:</p>
<ul>
<li>rejestrację użytkownika</li>
<li>weryfikację adresu e-mail</li>
<li>logowanie</li>
<li>sesje</li>
<li>wylogowanie z jednej lub wszystkich sesji</li>
<li>zmianę hasła</li>
<li>reset hasła</li>
<li>usunięcie konta</li>
<li>role i uprawnienia</li>
</ul>
<p>Celowo nie udostępnia własnych endpointów HTTP.</p>
<p>Dzięki temu kod korzystający z rdzenia może wyglądać następująco:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">register</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="o">=</span><span class="s2">&#34;*Emails are not allowed*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;StrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">verify_email</span><span class="p">(</span><span class="n">verification_token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">session_token</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">login</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;StrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">current_user</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">get_current_user</span><span class="p">(</span><span class="n">session_token</span><span class="p">)</span>
</span></span></code></pre></div><p>Tej samej instancji <code>UserHarbor</code> można używać w FastAPI, Flasku, Django, aplikacji CLI albo w programie, który w ogóle nie udostępnia HTTP.</p>
<p>Autoryzacja działa według tej samej zasady:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">roles</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="s2">&#34;admin&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">permissions</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="s2">&#34;users.delete&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">roles</span><span class="o">.</span><span class="n">grant_permission</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;admin&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;users.delete&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">grant_role</span><span class="p">(</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span> <span class="s2">&#34;admin&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">harbor</span><span class="o">.</span><span class="n">has_permission</span><span class="p">(</span><span class="n">session_token</span><span class="p">,</span> <span class="s2">&#34;users.delete&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">delete_user</span><span class="p">()</span>
</span></span></code></pre></div><p>Jeśli natomiast dostęp ma zostać wymuszony:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">require_permission</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">session_token</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;users.delete&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>UserHarbor implementuje prostą kontrolę dostępu opartą na rolach, ale pozostawia politykę autoryzacji właściwą dla aplikacji poza rdzeniem. Celowo nie próbuje stać się uniwersalnym silnikiem polityk.</p>
<h2 id="niezależność-od-frameworka-nie-oznacza-nieprzyjazności-wobec-frameworków">Niezależność od frameworka nie oznacza nieprzyjazności wobec frameworków</h2>
<p>Chciałem uniknąć sytuacji, w której niezależność od frameworka odbywa się kosztem wygody programisty.</p>
<p>Dlatego istnieje między innymi oficjalna integracja <code>userharbor-fastapi</code>.</p>
<p>Zamiast ręcznie pisać trasy uwierzytelniania i zależności, aplikacja FastAPI może skonfigurować UserHarbor i dołączyć adapter:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor</span> <span class="kn">import</span> <span class="n">UserHarbor</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor_fastapi</span> <span class="kn">import</span> <span class="n">UserHarborFastAPI</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span> <span class="o">=</span> <span class="n">UserHarbor</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">secret_key</span><span class="o">=</span><span class="s2">&#34;your-secret-key&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">store</span><span class="o">=</span><span class="n">store</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">=</span><span class="n">email_sender</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">auth</span> <span class="o">=</span> <span class="n">UserHarborFastAPI</span><span class="p">(</span><span class="n">harbor</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="n">app</span><span class="o">.</span><span class="n">include_router</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">auth</span><span class="o">.</span><span class="n">router</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">prefix</span><span class="o">=</span><span class="s2">&#34;/auth&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">tags</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;auth&#34;</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>Adapter dostarcza warstwę specyficzną dla frameworka: routery, schematy żądań, zależności uwierzytelniania bearer, mapowanie błędów oraz funkcje pomocnicze do wymagania określonych ról lub uprawnień.</p>
<p>Najważniejsze jest to, że FastAPI nadal nie przenika do rdzenia UserHarbor.</p>
<p>Możesz zmienić framework webowy bez zmiany logiki zarządzania kontami.</p>
<h2 id="trudniejszy-problem-co-znaczy-zaimplementować-userstore">Trudniejszy problem: co znaczy zaimplementować <code>UserStore</code>?</h2>
<p>Początkowo oddzielenie mechanizmu persystencji za interfejsem <code>UserStore</code> wydawało się oczywistym rozwiązaniem.</p>
<p>Definiujemy interfejs i implementujemy jego metody, dzięki czemu SQLAlchemy, MongoDB, Redis czy dowolne inne rozwiązanie może dostarczać warstwę przechowywania danych.</p>
<p>Istnieje jednak subtelny problem.</p>
<p>Zgodność sygnatur metod nie oznacza, że dwie implementacje warstwy danych zachowują się tak samo.</p>
<p>Weźmy pod uwagę token resetowania hasła.</p>
<p>Czy utworzenie nowego tokenu powinno usunąć poprzedni?</p>
<p>Co dzieje się po usunięciu użytkownika?</p>
<p>Czy jego sesje powinny zniknąć automatycznie?</p>
<p>Co powinno się stać, jeśli transakcja nie powiedzie się w połowie zmiany hasła?</p>
<p>Czy próba usunięcia czegoś, co już nie istnieje, powinna zgłosić błąd?</p>
<p>Te zachowania są częścią kontraktu warstwy danych, choć system typów Pythona nie potrafi ich wyrazić.</p>
<p>Stało się to jedną z najważniejszych zmian w UserHarbor 0.7.0.</p>
<h2 id="zamiana-kontraktu-adaptera-w-wykonywalne-testy">Zamiana kontraktu adaptera w wykonywalne testy</h2>
<p>UserHarbor zawiera teraz zestaw testów kontraktowych wielokrotnego użytku dla implementacji <code>UserStore</code>.</p>
<p>Adapter może zaimportować cały zestaw:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="c1"># tests/test_user_store_contract.py</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor.testing.user_store_contract</span> <span class="kn">import</span> <span class="o">*</span>
</span></span></code></pre></div><p>Musi jedynie dostarczyć czystą instancję warstwy danych:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">user_store</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">store</span> <span class="o">=</span> <span class="n">create_user_store</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">store</span>
</span></span><span class="line"><span class="cl">    <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">dispose_user_store</span><span class="p">(</span><span class="n">store</span><span class="p">)</span>
</span></span></code></pre></div><p>Te same testy można następnie uruchamiać dla SQLAlchemy, implementacji przechowującej dane w pamięci lub zupełnie innego backendu persystencji.</p>
<p>Wersja 0.7.0 wprowadziła <strong>57 testów kontraktowych wielokrotnego użytku</strong>, obejmujących użytkowników, hasze haseł, tokeny weryfikacji i resetu hasła, sesje, role, uprawnienia, relacje oraz zachowanie transakcji.</p>
<p>Zmienia to znaczenie adaptera.</p>
<p>Zgodny <code>UserStore</code> nie tylko deklaruje implementację interfejsu. Może wykazać, że przestrzega semantyki zachowań oczekiwanej przez rdzeń.</p>
<p>Kontrakt określa na przykład, że:</p>
<ul>
<li>nazwy użytkowników i adresy e-mail są unikalne</li>
<li>utworzenie użytkownika i początkowego tokenu weryfikacyjnego jest atomowe</li>
<li>nowy token weryfikacyjny zastępuje poprzedni</li>
<li>nowy token resetowania hasła zastępuje poprzedni</li>
<li>usunięcie użytkownika usuwa jego sesje i powiązane tokeny</li>
<li>wielokrotne przypisanie tej samej relacji jest idempotentne</li>
<li>usunięcie ról i uprawnień usuwa także ich przypisania</li>
<li>pomyślne transakcje są zatwierdzane</li>
<li>nieudane transakcje są wycofywane</li>
<li>zagnieżdżone transakcje uczestniczą w transakcji zewnętrznej</li>
</ul>
<p>Podczas implementowania kolejnego backendu łatwo przeoczyć te szczegóły. To właśnie takie różnice mogą powodować błędy uwierzytelniania, które ujawniają się dopiero znacznie później.</p>
<p>Dla mnie był to ważny krok w rozwoju architektury: abstrakcję opisują teraz nie tylko protokoły Pythona i dokumentacja, lecz również wykonywalne zachowanie.</p>
<h2 id="szablon-do-budowania-nowych-adapterów-warstwy-danych">Szablon do budowania nowych adapterów warstwy danych</h2>
<p>Aby uprościć ten proces, stworzyłem także <code>userharbor-inmemory</code>.</p>
<p>Jest to minimalna implementacja <code>UserStore</code>, która przechowuje dane w pamięci i przechodzi cały kontrakt warstwy danych.</p>
<p>Pełni dwie funkcje.</p>
<p>Po pierwsze, przydaje się w testach i przykładach, które nie wymagają prawdziwej bazy danych.</p>
<p>Po drugie, samo repozytorium może służyć jako szablon do tworzenia nowych integracji warstwy danych. Programista może zacząć od działającej implementacji i przechodzących testów kontraktowych, stopniowo zastępować backend in-memory, a przy tym stale sprawdzać, czy nowy adapter nadal zachowuje się poprawnie.</p>
<p>Chodzi o to, by tworzenie przyszłego adaptera bazy danych sprowadzało się przede wszystkim do następującego procesu:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">działający szablon UserStore
</span></span><span class="line"><span class="cl">        │
</span></span><span class="line"><span class="cl">        ▼
</span></span><span class="line"><span class="cl">zastąpienie implementacji persystencji
</span></span><span class="line"><span class="cl">        │
</span></span><span class="line"><span class="cl">        ▼
</span></span><span class="line"><span class="cl">uruchomienie wspólnych testów kontraktowych
</span></span><span class="line"><span class="cl">        │
</span></span><span class="line"><span class="cl">        ▼
</span></span><span class="line"><span class="cl">dodanie testów specyficznych dla backendu
</span></span></code></pre></div><p>zamiast odtwarzania oczekiwanego zachowania na podstawie implementacji rdzenia.</p>
<h2 id="zachowanie-związane-z-bezpieczeństwem-pozostaje-w-rdzeniu">Zachowanie związane z bezpieczeństwem pozostaje w rdzeniu</h2>
<p>Kolejną ważną granicą architektury jest to, że adaptery warstwy danych nigdy nie otrzymują surowych tokenów do zapisania.</p>
<p>UserHarbor generuje surowe tokeny weryfikacyjne, resetowania hasła i sesji, ale haszuje je przed przekazaniem do <code>UserStore</code>.</p>
<p>Surowy token trafia wyłącznie do tej części aplikacji, która go potrzebuje — na przykład do mechanizmu wysyłania e-maili albo do użytkownika po zalogowaniu.</p>
<p>Baza danych przechowuje hasz.</p>
<p>Rdzeń odpowiada także za zachowania takie jak wygasanie tokenów i walidacja sesji.</p>
<p>Operacje, które mogłyby ujawniać istnienie konta, w odpowiednich miejscach zwracają neutralne odpowiedzi. Przykładowo prośba o reset hasła dla nieznanego adresu e-mail nie zdradza, czy konto o takim adresie istnieje.</p>
<p>Istnieją też powiadomienia o zdarzeniach w cyklu życia konta, takich jak:</p>
<ul>
<li>udana weryfikacja adresu e-mail</li>
<li>zmiana hasła</li>
<li>reset hasła</li>
<li>usunięcie konta</li>
</ul>
<p>Interfejs <code>EmailSender</code> decyduje, w jaki sposób wiadomości zostaną dostarczone, ale nie rozstrzyga, kiedy dana operacja jest prawidłowa. Ta decyzja pozostaje w rdzeniu.</p>
<h2 id="sqlalchemy-bez-przejmowania-kontroli-nad-aplikacją">SQLAlchemy bez przejmowania kontroli nad aplikacją</h2>
<p>Oficjalny adapter SQLAlchemy jest gotowy do użycia, lecz jednym z wymagań projektowych było to, by wdrożenie UserHarbor nie zmuszało aplikacji do przyjęcia całkowicie osobnego modelu użytkownika.</p>
<p>Domyślnie adapter może zarządzać własną tabelą użytkowników.</p>
<p>Aplikacje, które mają już model SQLAlchemy, mogą jednak przekazać go adapterowi:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="n">store</span> <span class="o">=</span> <span class="n">SQLAlchemyUserStore</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">SessionLocal</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">user_model</span><span class="o">=</span><span class="n">AppUser</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>Aplikacja może również mapować swój model na bogatszy publiczny obiekt użytkownika, zamiast ograniczać się do minimalnej reprezentacji UserHarbor.</p>
<p>Dokumentacja opisuje teraz także użycie adaptera z migracjami Alembic zamiast polegania na <code>metadata.create_all()</code> podczas uruchamiania aplikacji.</p>
<p>To rozróżnienie ma znaczenie: przykłady powinny być łatwe do uruchomienia, ale prawdziwe aplikacje potrzebują rozsądnego sposobu zarządzania zmianami schematu.</p>
<h2 id="instalacja">Instalacja</h2>
<p>Sam rdzeń można zainstalować następująco:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install userharbor
</span></span></code></pre></div><p>Można też dołączyć wybrane oficjalne integracje:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install <span class="s2">&#34;userharbor[sqlalchemy,smtp,fastapi]&#34;</span>
</span></span></code></pre></div><p>Aby przetestować kompletny oficjalny zestaw:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install <span class="s2">&#34;userharbor[all]&#34;</span>
</span></span></code></pre></div><p>Główne oficjalne integracje obejmują obecnie warstwę danych SQLAlchemy, wysyłkę wiadomości przez SMTP oraz obsługę FastAPI.</p>
<h2 id="czym-userharbor-celowo-nie-powinien-się-stać">Czym UserHarbor celowo nie powinien się stać</h2>
<p>Niekontrolowane rozrastanie się zakresu funkcji to łatwa pułapka w tego rodzaju projekcie.</p>
<p>Gdy mamy już uwierzytelnianie, kusi nas dodanie OAuth. Następnie logowania społecznościowego. Potem MFA, organizacji, zespołów, list ACL, własności zasobów, paneli administracyjnych i języka polityk.</p>
<p>W końcu „mała biblioteka uwierzytelniania” staje się frameworkiem aplikacyjnym.</p>
<p>Właśnie tego staram się uniknąć.</p>
<p>Rdzeń powinien pozostać skoncentrowany na typowych, podstawowych operacjach zarządzania kontami.</p>
<p>Bardziej wyspecjalizowane funkcje mogą powstawać jako integracje lub osobne biblioteki, jeśli pojawi się na nie zapotrzebowanie. Nie powinny jednak komplikować podstawowego pakietu wszystkim jego użytkownikom.</p>
<p>Jedna z zasad projektowych UserHarbor jest więc celowo nudna:</p>
<p><strong>stabilność jest ważniejsza niż liczba funkcji.</strong></p>
<p>Gdy publiczne API się ustabilizuje, wolę przeznaczać czas na bezpieczeństwo, niezawodność, zgodność i wydajność niż nieustannie poszerzać zakres rdzenia.</p>
<h2 id="obecny-stan">Obecny stan</h2>
<p>UserHarbor znajduje się obecnie w wersji <strong>0.7.0</strong>.</p>
<p>Projekt jest wciąż młody i nie uważam jeszcze ani jego API za stabilne, ani samej biblioteki za gotową do zastosowań produkcyjnych.</p>
<p>Na tym etapie projekt służy w równym stopniu sprawdzaniu architektury, co dodawaniu funkcji.</p>
<p>Szczególnie zależy mi na opinii dotyczącej:</p>
<ul>
<li>granicy pomiędzy rdzeniem a integracjami</li>
<li>kontraktu <code>UserStore</code></li>
<li>podejścia opartego na testach kontraktowych</li>
<li>integracji z FastAPI</li>
<li>własnych backendów warstwy danych</li>
<li>kształtu publicznego API</li>
<li>tego, co powinno — a co nie powinno — należeć do rdzenia</li>
</ul>
<p>W Pythonie istnieje wiele dobrych bibliotek uwierzytelniania przeznaczonych dla konkretnych frameworków.</p>
<p>UserHarbor bada nieco inne pytanie:</p>
<p><strong>Czy sama domena zarządzania kontami może być wielokrotnie wykorzystywana z różnymi frameworkami i infrastrukturą, podczas gdy integracje pozostają małe, wymienne i niezależnie testowalne?</strong></p>
<p>Jak dotąd uważam, że właśnie ta granica okazuje się najciekawszą częścią projektu.</p>
<h2 id="linki">Linki</h2>
<p>Dokumentacja:<br>
<a href="https://userharbor.github.io/userharbor/">https://userharbor.github.io/userharbor/</a></p>
<p>Repozytorium:<br>
<a href="https://github.com/userharbor/userharbor">https://github.com/userharbor/userharbor</a></p>
<p>Integracja FastAPI:<br>
<a href="https://github.com/userharbor/userharbor-fastapi">https://github.com/userharbor/userharbor-fastapi</a></p>
<p>Adapter SQLAlchemy:<br>
<a href="https://github.com/userharbor/userharbor-sqlalchemy">https://github.com/userharbor/userharbor-sqlalchemy</a></p>
<p>Adapter SMTP:<br>
<a href="https://github.com/userharbor/userharbor-smtp">https://github.com/userharbor/userharbor-smtp</a></p>
<p><em>Artykuł pierwotnie opublikowany po angielsku na <a href="https://coderlegion.com/24763/building-userharbor-framework-agnostic-user-management-for-python">CoderLegion</a>.</em></p>
]]></content:encoded>
    </item>
    <item>
      <title>Stworzyłem UserHarbor — niezależną od frameworka bibliotekę do zarządzania użytkownikami w Pythonie</title>
      <link>https://spaceshaman.github.io/pl/posts/i-built-userharbor-a-framework-agnostic-user-management-library-for-python/</link>
      <pubDate>Wed, 01 Jul 2026 17:19:53 +0000</pubDate>
      <guid>https://spaceshaman.github.io/pl/posts/i-built-userharbor-a-framework-agnostic-user-management-library-for-python/</guid>
      <description>Dlaczego stworzyłem UserHarbor i oddzieliłem logikę zarządzania użytkownikami od frameworków webowych, baz danych, ORM-ów i dostawców poczty.</description>
      <content:encoded><![CDATA[<p>Pracując ostatnio nad aplikacją SaaS, po raz kolejny musiałem zaimplementować ten sam zestaw funkcji związanych z kontami użytkowników:</p>
<ul>
<li>rejestrację</li>
<li>logowanie</li>
<li>sesje</li>
<li>weryfikację adresu e-mail</li>
<li>reset hasła</li>
<li>zmianę hasła</li>
<li>usuwanie konta</li>
<li>podstawowe role i uprawnienia</li>
</ul>
<p>Żadna z tych rzeczy nie była szczególnie trudna.</p>
<p>Była za to powtarzalna.</p>
<p>Pisałem już wcześniej podobny kod i nie chciałem w każdym nowym projekcie w Pythonie odtwarzać od podstaw tej samej warstwy zarządzania użytkownikami.</p>
<p>Zacząłem więc pracować nad <strong>UserHarbor</strong>.</p>
<h2 id="czym-jest-userharbor">Czym jest UserHarbor?</h2>
<p><strong>UserHarbor</strong> to niezależna od frameworka biblioteka Pythona służąca do zarządzania kontami użytkowników.</p>
<p>Założenie jest proste:</p>
<blockquote>
<p>rdzeń powinien pozostać mały, przewidywalny i niezależny od konkretnego frameworka webowego, bazy danych, ORM-u czy dostawcy poczty.</p>
</blockquote>
<p>Rdzeń obsługuje logikę zarządzania kontami.
Integracjami zajmują się osobne pakiety adapterów.</p>
<p>Zamiast tworzyć rozwiązanie wyłącznie dla FastAPI, Flaska, Django czy jednego konkretnego stosu technologicznego, chciałem zbudować rdzeń nadający się do użycia w różnych rodzajach aplikacji w Pythonie.</p>
<p>Na przykład w:</p>
<ul>
<li>aplikacjach FastAPI</li>
<li>aplikacjach Flask</li>
<li>aplikacjach Django</li>
<li>narzędziach CLI</li>
<li>narzędziach wewnętrznych</li>
<li>własnych usługach w Pythonie</li>
</ul>
<h2 id="dlaczego-nie-użyć-po-prostu-biblioteki-dla-konkretnego-frameworka">Dlaczego nie użyć po prostu biblioteki dla konkretnego frameworka?</h2>
<p>Istnieją już dobre narzędzia przeznaczone dla konkretnych frameworków.</p>
<p>Mnie jednak zależało na czymś nieco innym.</p>
<p>Nie chciałem, aby logika zarządzania użytkownikami była ściśle powiązana z:</p>
<ul>
<li>frameworkiem webowym</li>
<li>warstwą bazy danych</li>
<li>dostawcą poczty</li>
<li>konkretnym modelem żądań i odpowiedzi</li>
</ul>
<p>Zamiast tego UserHarbor używa niewielkich interfejsów dla takich elementów jak przechowywanie danych i wysyłanie wiadomości e-mail.</p>
<p>Główne interfejsy to:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">UserStore</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">EmailSender</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span></code></pre></div><p>Rdzenia nie interesuje, w jaki sposób użytkownicy są przechowywani ani jak wysyłane są wiadomości e-mail.</p>
<p>Za tę część odpowiadają adaptery.</p>
<h2 id="instalacja">Instalacja</h2>
<p>Jeśli chcesz dostarczyć własne implementacje <code>UserStore</code> i <code>EmailSender</code>, zainstaluj tylko pakiet rdzenia:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install userharbor
</span></span></code></pre></div><p>Aby zainstalować rdzeń wraz z oficjalnymi adapterami SQLAlchemy, SMTP i FastAPI:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install <span class="s2">&#34;userharbor[sqlalchemy,smtp,fastapi]&#34;</span>
</span></span></code></pre></div><p>Możesz też od razu zainstalować wszystkie oficjalne integracje:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install <span class="s2">&#34;userharbor[all]&#34;</span>
</span></span></code></pre></div><h2 id="oficjalne-adaptery">Oficjalne adaptery</h2>
<p>Obecnie dostępnych jest kilka oficjalnych pakietów adapterów:</p>
<ul>
<li><code>userharbor-sqlalchemy</code> — przechowywanie danych za pomocą SQLAlchemy</li>
<li><code>userharbor-smtp</code> — wysyłanie wiadomości e-mail przez SMTP</li>
<li><code>userharbor-fastapi</code> — integracja z FastAPI</li>
</ul>
<p>Dzięki temu rdzeń pozostaje mały, a jednocześnie typową konfigurację można łatwo zainstalować i uruchomić.</p>
<h2 id="szybki-przykład">Szybki przykład</h2>
<p>Oto dłuższy przykład wykorzystujący SQLAlchemy do przechowywania danych i SMTP do wysyłania wiadomości e-mail:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy.orm</span> <span class="kn">import</span> <span class="n">sessionmaker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor</span> <span class="kn">import</span> <span class="n">UserHarbor</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor_sqlalchemy</span> <span class="kn">import</span> <span class="n">SQLAlchemyUserStore</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor_smtp</span> <span class="kn">import</span> <span class="n">SMTPEmailSender</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="s2">&#34;sqlite:///users.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">SessionLocal</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">=</span> <span class="n">SQLAlchemyUserStore</span><span class="p">(</span><span class="n">SessionLocal</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">store</span><span class="o">.</span><span class="n">metadata</span><span class="o">.</span><span class="n">create_all</span><span class="p">(</span><span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">email_sender</span> <span class="o">=</span> <span class="n">SMTPEmailSender</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">host</span><span class="o">=</span><span class="s2">&#34;smtp.example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">port</span><span class="o">=</span><span class="mi">587</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="s2">&#34;smtp-user&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;smtp-password&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">from_email</span><span class="o">=</span><span class="s2">&#34;noreply@example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span> <span class="o">=</span> <span class="n">UserHarbor</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">secret_key</span><span class="o">=</span><span class="s2">&#34;your-secret-key&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">store</span><span class="o">=</span><span class="n">store</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">=</span><span class="n">email_sender</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Rejestracja użytkownika</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">register</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">email</span><span class="o">=</span><span class="s2">&#34;jane@example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;StrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Weryfikacja adresu e-mail</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">verify_email</span><span class="p">(</span><span class="s2">&#34;verification-token-from-email&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Logowanie</span>
</span></span><span class="line"><span class="cl"><span class="n">session_token</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">login</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;StrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Weryfikacja sesji</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">harbor</span><span class="o">.</span><span class="n">verify_session</span><span class="p">(</span><span class="n">session_token</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Użytkownik jest zalogowany&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Pobranie bieżącego użytkownika</span>
</span></span><span class="line"><span class="cl"><span class="n">current_user</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">get_current_user</span><span class="p">(</span><span class="n">session_token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">current_user</span><span class="o">.</span><span class="n">username</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Utworzenie ról i uprawnień</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">roles</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="s2">&#34;admin&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">permissions</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="s2">&#34;users.delete&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">roles</span><span class="o">.</span><span class="n">grant_permission</span><span class="p">(</span><span class="s2">&#34;admin&#34;</span><span class="p">,</span> <span class="s2">&#34;users.delete&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">grant_role</span><span class="p">(</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span> <span class="s2">&#34;admin&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Sprawdzenie dostępu</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="n">harbor</span><span class="o">.</span><span class="n">has_permission</span><span class="p">(</span><span class="n">session_token</span><span class="p">,</span> <span class="s2">&#34;users.delete&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Użytkownik może usuwać innych użytkowników&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">current_admin</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">require_role</span><span class="p">(</span><span class="n">session_token</span><span class="p">,</span> <span class="s2">&#34;admin&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">current_admin</span><span class="o">.</span><span class="n">username</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Wylogowanie</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">logout</span><span class="p">(</span><span class="n">session_token</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Zmiana hasła</span>
</span></span><span class="line"><span class="cl"><span class="n">session_token</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">login</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;StrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">change_password</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">old_password</span><span class="o">=</span><span class="s2">&#34;StrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">new_password</span><span class="o">=</span><span class="s2">&#34;EvenStrongerPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">session_token</span><span class="o">=</span><span class="n">session_token</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Wysłanie wiadomości umożliwiającej zresetowanie hasła</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">send_password_reset</span><span class="p">(</span><span class="s2">&#34;jane@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Reset hasła</span>
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">reset_password</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">new_password</span><span class="o">=</span><span class="s2">&#34;NewStrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">reset_token</span><span class="o">=</span><span class="s2">&#34;reset-token-from-email&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># Usunięcie konta</span>
</span></span><span class="line"><span class="cl"><span class="n">session_token</span> <span class="o">=</span> <span class="n">harbor</span><span class="o">.</span><span class="n">login</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="s2">&#34;jane&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;NewStrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span><span class="o">.</span><span class="n">delete_account</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="s2">&#34;NewStrongPassword123!&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">session_token</span><span class="o">=</span><span class="n">session_token</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><h2 id="pełny-przykład-fastapi-z-oficjalnymi-integracjami">Pełny przykład FastAPI z oficjalnymi integracjami</h2>
<p>Jeśli chcesz wypróbować pełną konfigurację z FastAPI, SQLAlchemy i SMTP, zainstaluj wszystkie oficjalne integracje:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install <span class="s2">&#34;userharbor[all]&#34;</span>
</span></span></code></pre></div><p>Następnie utwórz aplikację FastAPI:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy.orm</span> <span class="kn">import</span> <span class="n">sessionmaker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor</span> <span class="kn">import</span> <span class="n">UserHarbor</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor_fastapi</span> <span class="kn">import</span> <span class="n">UserHarborFastAPI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor_smtp</span> <span class="kn">import</span> <span class="n">SMTPEmailSender</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">userharbor_sqlalchemy</span> <span class="kn">import</span> <span class="n">SQLAlchemyUserStore</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="s2">&#34;sqlite:///users.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">SessionLocal</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">store</span> <span class="o">=</span> <span class="n">SQLAlchemyUserStore</span><span class="p">(</span><span class="n">SessionLocal</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">store</span><span class="o">.</span><span class="n">metadata</span><span class="o">.</span><span class="n">create_all</span><span class="p">(</span><span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">email_sender</span> <span class="o">=</span> <span class="n">SMTPEmailSender</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">host</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;HOST&#34;</span><span class="p">,</span> <span class="s2">&#34;smtp.example.com&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">port</span><span class="o">=</span><span class="nb">int</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;PORT&#34;</span><span class="p">,</span> <span class="mi">587</span><span class="p">)),</span>
</span></span><span class="line"><span class="cl">    <span class="n">username</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;USERNAME&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">password</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;PASSWORD&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="n">from_email</span><span class="o">=</span><span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;USERNAME&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">harbor</span> <span class="o">=</span> <span class="n">UserHarbor</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="n">secret_key</span><span class="o">=</span><span class="s2">&#34;your-secret-key&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">store</span><span class="o">=</span><span class="n">store</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">=</span><span class="n">email_sender</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">auth</span> <span class="o">=</span> <span class="n">UserHarborFastAPI</span><span class="p">(</span><span class="n">harbor</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span><span class="o">.</span><span class="n">include_router</span><span class="p">(</span><span class="n">auth</span><span class="o">.</span><span class="n">router</span><span class="p">,</span> <span class="n">prefix</span><span class="o">=</span><span class="s2">&#34;/auth&#34;</span><span class="p">,</span> <span class="n">tags</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;auth&#34;</span><span class="p">])</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="kn">import</span> <span class="nn">uvicorn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">uvicorn</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">app</span><span class="p">,</span> <span class="n">host</span><span class="o">=</span><span class="s2">&#34;0.0.0.0&#34;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">8000</span><span class="p">)</span>
</span></span></code></pre></div><h2 id="zasady-projektowe">Zasady projektowe</h2>
<p>Projekt opiera się na kilku założeniach.</p>
<h3 id="rdzeń-powinien-pozostać-mały">Rdzeń powinien pozostać mały</h3>
<p>UserHarbor nie ma stać się kompletną platformą do zarządzania tożsamością.</p>
<p>Rdzeń koncentruje się na podstawowych procesach związanych z zarządzaniem kontami:</p>
<ul>
<li>rejestracji</li>
<li>logowaniu</li>
<li>sesjach</li>
<li>weryfikacji adresu e-mail</li>
<li>resetowaniu hasła</li>
<li>zmianie hasła</li>
<li>usuwaniu konta</li>
<li>prostej kontroli dostępu opartej na rolach</li>
</ul>
<p>Wszystko, co jest wysoce specyficzne dla danej aplikacji, powinno pozostać poza rdzeniem.</p>
<h3 id="adaptery-powinny-znajdować-się-poza-rdzeniem">Adaptery powinny znajdować się poza rdzeniem</h3>
<p>Integracje z bazami danych, ORM-ami, pocztą i frameworkami powinny być osobnymi pakietami.</p>
<p>Dzięki temu rdzeń pozostaje niezależny, a inni mogą łatwiej tworzyć własne integracje.</p>
<p>Ktoś mógłby na przykład stworzyć:</p>
<ul>
<li><code>userharbor-redis</code></li>
<li><code>userharbor-mongodb</code></li>
<li><code>userharbor-sendgrid</code></li>
<li><code>userharbor-resend</code></li>
<li><code>userharbor-django</code></li>
<li><code>userharbor-flask</code></li>
</ul>
<p>bez modyfikowania głównego pakietu.</p>
<h3 id="api-powinno-być-nudne">API powinno być nudne</h3>
<p>Staram się, aby publiczne API było jawne i przewidywalne.</p>
<p>Bez ukrytej magii frameworka.
Bez narzuconego modelu bazy danych.
Bez zależności od jednego konkretnego sposobu budowania aplikacji w Pythonie.</p>
<h2 id="obecny-stan">Obecny stan</h2>
<p>Projekt jest nadal na wczesnym etapie rozwoju.</p>
<p>Podstawowe procesy działają, ale API nie jest jeszcze w pełni stabilne. Obecnie nie nazwałbym tej biblioteki gotową do zastosowań produkcyjnych.</p>
<p>W tej chwili zależy mi przede wszystkim na opiniach dotyczących:</p>
<ul>
<li>publicznego API</li>
<li>architektury adapterów</li>
<li>granicy pomiędzy rdzeniem a integracjami</li>
<li>integracji z SQLAlchemy</li>
<li>integracji z FastAPI</li>
<li>tego, czy prosty RBAC powinien należeć do rdzenia</li>
<li>tego, jak powinno wyglądać wygodne tworzenie własnych adapterów</li>
</ul>
<h2 id="linki">Linki</h2>
<p>Dokumentacja:<br>
<a href="https://userharbor.github.io/userharbor/">https://userharbor.github.io/userharbor/</a></p>
<p>Repozytorium:<br>
<a href="https://github.com/userharbor/userharbor">https://github.com/userharbor/userharbor</a></p>
<p>Integracja FastAPI:<br>
<a href="https://github.com/userharbor/userharbor-fastapi">https://github.com/userharbor/userharbor-fastapi</a></p>
<p>Adapter SQLAlchemy:<br>
<a href="https://github.com/userharbor/userharbor-sqlalchemy">https://github.com/userharbor/userharbor-sqlalchemy</a></p>
<p>Adapter SMTP:<br>
<a href="https://github.com/userharbor/userharbor-smtp">https://github.com/userharbor/userharbor-smtp</a></p>
<h2 id="opinie-mile-widziane">Opinie mile widziane</h2>
<p>Będę wdzięczny za wszelkie uwagi, zwłaszcza od osób, które wielokrotnie tworzyły w Pythonie mechanizmy zarządzania użytkownikami.</p>
<p>Czy takie podejście oparte na adapterach ma sens?</p>
<p>Czy spodziewalibyście się prostych ról i uprawnień w rdzeniu, czy raczej w osobnym pakiecie?</p>
<p>A gdybyście integrowali UserHarbor z własnym projektem, jakiego API byście oczekiwali?</p>
<p><em>Aktualizacja: później napisałem <a href="/pl/posts/building-userharbor-framework-agnostic-user-management-for-python/">bardziej szczegółowy artykuł o architekturze adapterów UserHarbor i wykonywalnym kontrakcie warstwy danych</a>.</em></p>
<p><em>Artykuł pierwotnie opublikowany po angielsku na <a href="https://dev.to/spaceshaman/i-built-userharbor-a-framework-agnostic-user-management-library-for-python-1mkj">DEV Community</a>.</em></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
