NET 6 WebAPI - powiązanie tokena z użytkownikiem

NET 6 WebAPI - powiązanie tokena z użytkownikiem
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Witam.
Potrzebuje stworzyć mały serwer licencji do oprogramowania okienkowego. Stanęło na API, które będzie przechowywać odpowiednie informację, z którym to aplikacja okienkowa będzie się komunikować i podejmować odpowiednie decyzje.

Oprócz jakichś zmyślnych "kluczy", chciałbym też generować token, który byłby powiązany z danym klientem i jego licencją. Na tej podstawie również może działać wygaśnięcie licencji, ponieważ ważności tokena może o tym dodatkowo decydować.

Czy jedynym wyjściem jest wpisanie tokenów do bazy?

ID VAT_NUMBER EMAIL TOKEN EXPIRES_AT

Domyślam się, że wtedy też trzeba będzie zrobić jakiś middleware do atrybutu [Authorize], albo własny [CustomAuthorize], który to poprawnie zweryfikuje i zwróci odpowiedni response code.

Ogólnie to chciałbym, aby token nie był "uniwersalny", że dochodzi do niestandardowej autoryzacji. Żeby inny user nie mógł użyć tokena drugiego.

Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

Dosyć to abstrakcyjne więc ciężko coś jasno odpowiedzieć. Masz na myśli używanie swojego własnego tokena? Rozważałeś użycie JWT? Wtedy nie będziesz musiał wpisywać tego do bazy, bo wszelkie potrzebne informacje możesz przekazać w formie claimów.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 1x, ostatnio: Aventus
_13th_Dragon
  • Rejestracja:ponad 19 lat
  • Ostatnio:3 miesiące
0

Najprostsze rozwiązanie: - W przypadku autoryzacji tokenem, przekazujesz ten jako login słowo np: "<Tokien>" zaś sam tokien przekazujesz jako hasło.
Więc tak jakby zostaje zalogowany użytkownik "<Tokien>".
W zasadzie jedyna zmiana w poleceniu SQL sprawdzającym czy można się zalogować.


Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Tak, JWT. Tylko czy takie dane nie są zbyt "otwarte"? Takie JWT można edytować za pomocą jwt.io/. Czy już za daleko z tym idę i nikt nie jest na tyle mądry? Robienie jakichś hashy na podstawie sprzętu chyba już jest staroświeckie. Wiem, że licencjonowanie "online" jest najlepsze, ponieważ niczego nie muszę hardcodować, a żaden cwaniak mi tego nie skopiuje i nie zacznie używać bez mojej wiedzy/zgody.

Stąd też dodatkowa weryfikacja za pomocą tokena. Czyli, aby oprogramowanie klientowi działało to potrzebuje mieć numer nip w bazie. W naszym przypadku jest on wpisany w Comarch Optima i ten sam nip byłby u nas w bazie API, email do powiadomień, ze subskrypcja się kończy no i token, który dodatkowo zabezpieczyłby sparowane oprogramowanie.

SZ
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 20 godzin
  • Postów:1492
1
AdamWox napisał(a):

Tak, JWT. Tylko czy takie dane nie są zbyt "otwarte"? Takie JWT można edytować za pomocą jwt.io/.

No możesz edytować ale nic Ci to nie da xD

_13th_Dragon
  • Rejestracja:ponad 19 lat
  • Ostatnio:3 miesiące
0

Skoro widzisz to jako dodatkowe zabezpieczenie to wszystko to nie ma żadnego sensu, tylko utrudnisz logowanie.


Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
edytowany 1x, ostatnio: _13th_Dragon
Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
4

Tokeny JWT można edytować, ale wtedy ich sygnatura nie będzie się zgadzać. Innymi słowy, weryfikując token sprawdzasz czy został on przez Ciebie podpisany, i niezmieniony. Token podpisuje się za pomocą sekretu, i sprawdza za pomocą tegoż sekretu (synchroniczny token) lub publicznego klucza (niesynchroniczny token). Oczywiście jeśli weryfikujesz za pomocą teog samego sekretu, to musi się to odbyć tylko przez serwer który ten sekret posiada i z nikim się nie dzieli. W przypadku kluczy publicznych sprawa jest prostsza, bo będziesz mógł go sprawdzić przez jakiegokolwiek klienta, nawet po stronie Twojej aplikacji okienkowej.

Ze względu na to że token nie jest szyfrowany, to ważne aby nie przechowywać w claimach żadnych sekretów/poufnych informacji.

EDIT: Inna sprawa żę JWT nie rozwiązuje wszystkich problemów, jeśli np. zależy Ci na odwoływaniu tokenów przed ich wygaśnięciem. Wtedy będziesz musiał mieć jakiś stan sesji na serwerze.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
edytowany 2x, ostatnio: Aventus
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Ok, teraz rozumiem. Czyli generalnie rzecz biorąc ten token nie jest mi potrzebny do niczego, oraz mogę zrezygnować z samego [Authorize] w API, ponieważ wtedy jeden i ten sam token mógłby działać u każdego z klientów? Nikt, ręcznie, nie będzie mógł tego tokena wygenerować, tylko ja, osobnym oprogramowaniem dostępnym u mnie na serwerze. Token generowałby się w momencie dopisywania nowego klienta lub podczas odświeżania w razie wygaśnięcia licencji.

EDIT
Jedyne co, to własną weryfikacje NIP, Email i innych zmyślnych głupot.

edytowany 1x, ostatnio: AdamWox
_13th_Dragon
  • Rejestracja:ponad 19 lat
  • Ostatnio:3 miesiące
0

Raczej tokien ma krótki żywot (przeważnie) ewentualnie jego czas życia określony przez samego użytkownika.


Wykonuję programy na zamówienie, pisać na Priv.
Asm/C/C++/Pascal/Delphi/Java/C#/PHP/JS oraz inne języki.
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Ja mogę zdefiniować w API jaki żywot ma mieć token.

Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
1

Tak, chociaż tak jak napisałem w edycji posta, wygasanie licencji wymagałoby dodatkowej obsługi po Twojej stronie. Token ma czas ważności, i raz wygenerowany z takim czasem ważności nie może zostać zmieniony. Więc bez podjęcia odpowiednich kroków, nawet jeśli w Twoim systemie już by nie było takiej licencji, techniczne token nadal byłby ważny.

Ja mogę zdefiniować w API jaki żywot ma mieć token.

A co jeśli okaże się że musisz anulować licencję wcześniej?


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

W tabeli miałby kolumnę ExpiresAt, która by przyjmowała datę wygaśnięcia licencji. Jeśli token byłby dalej ważny to nie przejdzie dalsza weryfikacja. To samo tyczyłoby się anulacji licencji.

SP
SP
  • Rejestracja:prawie 3 lata
  • Ostatnio:ponad 2 lata
  • Postów:181
1

Robisz na tej aplikacji zwykłe logowanie, po logowaniu możesz token zwrócić i refresh token, jeśli ktoś będzie używał dwóch tokenów to jak jeden wygaśnie, użyje refresh tokena, drugi ukradnięty token zostanie użyty to też wymusi refresh tokena przez co wykryjesz drugie użycie tokena refresh i możesz odciąć użytkownika bo ktoś mu się włamał.

Ale jak użytkownik kupił licencję to i tak musi mieć pełne prawo do produktu więc przy zalogowaniu musi mieć zawsze możliwość odzyskać dostęp.

Token JWT działa na zasadzie synchroniczny hmac-sha256, co w skrócie za pomocą hasła skomplikowanego klucza randomowego, wykonujesz podpis na danych jego login/id, exp/expires i tu podajesz jak długo ma być żywy token.
Token dajesz użytkownikowi.

Jak użytkownik wyśle token z zapytaniem do api, to serwer sprawdza czy podpis się zgadza za pomocą hmac-sha256 potem sprawdza expires czy nie wygasł i jak jest wszystko git to daje dostęp.


Knowledge Distiller
Aventus
Pominąłeś scenariusz w którym licencja wygasa/zostaje anulowana wcześniej niż token.
SP
Szalony Programista2
A w sumie myślałem, że historię zakupów licencji będzie trzymał na swoim serwerze w bazie, jak aplikacja nie będzie wymagała api z jakimiś danymi do działania to i tak wszystko będzie podane na tacy i pewnie pójdzie obejść i tak.
Aventus
  • Rejestracja:około 9 lat
  • Ostatnio:ponad 2 lata
  • Lokalizacja:UK
  • Postów:2235
0
AdamWox napisał(a):

W tabeli miałby kolumnę ExpiresAt, która by przyjmowała datę wygaśnięcia licencji. Jeśli token byłby dalej ważny to nie przejdzie dalsza weryfikacja. To samo tyczyłoby się anulacji licencji.

To oznacza że każda weryfikacja będzie się musiała odbywać poprzez komunikację z serwerem (wspomniana wcześniej przeze mnie stanowość). Niekoniecznie to coś złego- po prostu coś z czego należy sobie zdawać sprawę. W takim przypadku faktycznie równie dobrze mógłbyś się obejść bez JWT, i stworzyć jakiś własny, uproszczony token.

Tylko mam wrażenie że zatoczyłeś koło, bo przecież w pierwszym poście sugerowałeś że chcesz uniknąć trzymania czegoś w bazie.


Na każdy złożony problem istnieje rozwiązanie które jest proste, szybkie i błędne.
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Bardziej mnie martwiło bezpieczeństwo trzymania tokenów w bazie, stąd pytanie czy to jedyne wyjście. A, że takiego typu licencjonowanie i zabezpieczenia robię pierwszy raz to chciałbym obrać jakiś jeden kierunek na przyszłość i tylko dopisywać licencje do odpowiedniego oprogramowania.

PS.
Wątpię, aby ktoś z naszym oprogramowaniem chciał się bawić w nielegalne kopiowanie. Szefy się uparły, że ma być, bo myślą, że ten program jest złotem i każdy będzie je chciał 😅

SP
Szalony Programista2
żeby wykryć kradzież tokena będziesz musiał trzymać historię refresh tokenów, ale jak dasz użytkownikowi całą aplikację, w której będzie wszystko potrzebne do działania i nie będzie niczego z neta potrzebował to i tak może sobie zcrackować. Tokenów raczej nie trzeba nigdzie trzymać bo one same się udowadniają, trzymasz tylko sekret i jak po wykonaniu hmac-sha256 z tym sekretem na tokenie, który ci wysłał użytkownik nie wyjdzie ci taka sama sygnatura jaką ci przysłał użytkownik, a on nie zna sekretu żeby taki token zrobić, to wiesz, że jest invalid.
SZ
  • Rejestracja:prawie 11 lat
  • Ostatnio:około 20 godzin
  • Postów:1492
0

Mi się wydaje, że nie jest potrzebny żaden JWT.
Pytanie jest takie jak chcesz, żeby to działało po zarejestrowaniu. Czy przy każdym uruchomieniu ma walidować ważność licencji?

Kiedyś siedziałem w nauce jazdy. Kupowałem program do egzaminu wewnętrznego. Opierało się to na podobnym rozwiązaniu. Tzn Uruchamiales program i wymagał podania klucza licencyjnego, Po wklepaniu klucza leciał request do serwera licencyjnego. Wracała odpowiedź że ważdny to tego i tego dnia. Potem czy offline czy online program działał. Ale jak np usunąłem program i ponownie zainstalowałem to nie mogłem użyć drugi raz tej licencji tylko był telefon do firmy o odblokowanie w celu ponownej rejestracji.

SP
Szalony Programista2
W tym wypadku, program do tej nauki miał algorytm do wyliczania licencji, który można odwrócić i zrobić generator dowolnej lub ze zmiany warunku sprawdzającego licencję na instrukcję NOP no operations. Ale zwykle takie zabezpieczenia wystarczają, jeszcze trochę zaciemnienia kodu, obfuskacji i może nikomu nie będzie się chciało przez to przebijać.
SZ
No nie zrobisz generatora bo licencja była zarejestrowana na konkretną firme. I jeśli producent nie miał takich danych na serwerze licencyjnym to generowanie ponownej licencji nic nie dało
SP
Szalony Programista2
Jeśli musiałeś na serwer wysyłać licencje to tak, jeśli lokalnie aplikacja sprawdzała czy się zgadza to nie.
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Nie bardzo mogę to w ten sposób zrobić, ponieważ to są dwa programy, jeden jest usługą windows, który ogarnia całą logikę. Okno jest tylko do konfiguracji tej usługi. Zapytanie o ważność licencji leciałoby przy każdym wywołaniu zadania. Oprogramowanie bazuje na danych z internetu - Allegro, Baselinker, IdoSell, więc brak internetu nie popsuję licencjonowania, ponieważ brak połączenia do serwera licencji będzie najmniejszym problemem jeśli żadne dane nie zostaną przetworzone. Teraz teoretycznie wszystko zależy od instalatora - jeśli ktoś przeinstaluje program na tej samej maszynie to konfiguracja zostanie i licencja będzie aktywna. Jeśli zainstaluje na innym komputerze to trzeba będzie nową licencję, albo istniejącą zamienić na nowym sprzęcie.

AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Zastanawia mnie jeszcze jedno. Biorąc przykład Allegro:

  1. Token jest powiązany z danym kontem użytkownika
  2. Jeśli pobiorę zamówienie używając tokenu z innego konta to dostaje NotFound

Jak taki system łączy to wszystko? Czy to nie jest przypadkiem to samo co ja chciałem uzyskać?

G1
  • Rejestracja:około 4 lata
  • Ostatnio:7 dni
  • Postów:506
0

Ja bym zrobił to najprościej jak się da z użyciem JWT:

  1. Dodajesz do dbContext tabelę:
Kopiuj
public class Subscription
    {
        public Guid Id { get; set; }
        public Guid ApplicationUserId { get; set; }
        public DateTime? ExpiryTime { get; set; }
    }
  1. W akcji API dodajesz [Authorize(AuthenticationSchemes = "Bearer")], nie zapominając o AddJwtBearer()

  2. Na podstawie zalogowanego użytkownika pobierasz licencje, które posiada i tyle w temacie. :)

Kopiuj
var currentUser = await _userManager.GetUserAsync(User);
var activeSubsriptions = await _subscriptionManager.GetAllActiveForUser(currentUser.Id);
AdamWox
  • Rejestracja:ponad 7 lat
  • Ostatnio:około 12 godzin
  • Lokalizacja:Jastrzębie-Zdrój
  • Postów:2161
0

Po pierwsze nie używam EF, ale to akurat ma najmniejsze znaczenie. Po drugie, nikt się nigdzie nie loguje w takim sensie, że nie ma do tego żadnego frontu. API w tym przypadku jest transparentne A atrybut [Authorize] zadziała dla każdego kto ma token. Ja chce żeby API rzuciło dane tylko tego użytkownika, którego jest token.

USER 1 => token XYZ => nip 123
USER 2 => token ABC => nip 456

Jeśli odpytam API podając nip 456 ale używając tokena XYZ, to powinno zwrócić 404

G1
  • Rejestracja:około 4 lata
  • Ostatnio:7 dni
  • Postów:506
1

Nieistotne jakiej bazy używasz. Daję Tobie przykład dla Jeśli odpytam API podając nip 456 ale używając tokena XYZ, to powinno zwrócić 404

Kopiuj
public class Subscription
    {
        public Guid Id { get; set; }
        public Guid ApplicationUserId { get; set; }
        public string NIP { get; set; }
        public DateTime? ExpiryTime { get; set; }
    }
 [Authorize(AuthenticationSchemes = "Bearer")]
 public async Task<IActionResult> Example([FromQuery]ExampleInput input)
 {
    var currentUser = await _userManager.GetUserAsync(User);
    var activeSubsriptions = await _subscriptionManager.GetAllActiveForUser(currentUser.Id, input.NIP);
    if(activeSubsriptions == null)
      return NotFound();

Po prostu stwórz ten token ręcznie i wczytaj dynamicznie do aplikacji

edytowany 1x, ostatnio: gswidwa1
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)