diff --git a/common/static/common/css/common.css b/common/static/common/css/common.css
index f54a0205b9e1235f23956ffb8a0c6238bc4c57aa..fa4ecbd776c4d79caabb1fc60459500ccfc24d63 100644
--- a/common/static/common/css/common.css
+++ b/common/static/common/css/common.css
@@ -29,6 +29,11 @@ main {
     max-width: 500px;
 }
 
+.btn-xs {
+    padding: .2rem .4rem;
+    line-height: 0.8rem;
+}
+
 /* TODO: doesn’t work under Firefox 89, possibly older too */
 ::-webkit-scrollbar {
     width: 4px;
diff --git a/common/static/common/js/utils.js b/common/static/common/js/utils.js
index 2c5106e0e01159435640351f641ad386fb3ab7b8..8e8763c8ea943aa416c74b3fb6d32c7ab5f47464 100644
--- a/common/static/common/js/utils.js
+++ b/common/static/common/js/utils.js
@@ -15,7 +15,7 @@ function tooltipped_span(text, tooltip_text, cls) {
 }
 
 function tooltipped_info(text) {
-    return tooltipped_span('<img src="/static/common/img/info.svg" alt="info" width="14" height="14"/>', text);
+    return tooltipped_span('<img src="' + window.STATIC_URL + 'common/img/info.svg" alt="info" width="14" height="14"/>', text);
 }
 
 function activate_tooltips(selector) {
diff --git a/common/templates/base.html b/common/templates/base.html
index a24c6112dc3a039d5fc7cc8c322cb8d670730a43..b041c547547c78fcabd6f7a31f3eb00164a63f62 100644
--- a/common/templates/base.html
+++ b/common/templates/base.html
@@ -10,7 +10,7 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
     <title>{% block title %}{% endblock %} – Walenty [beta]</title>
-    <link rel="icon" href="/static/common/favicon.ico">
+    <link rel="icon" href="{% static 'common/favicon.ico' %}">
     <link rel="stylesheet" type="text/css" href="https://bootswatch.com/4/lux/bootstrap.min.css">
     <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Condensed:wght@300;400;700&display=swap">
     {% block styles %}{% endblock %}
@@ -22,6 +22,9 @@
     <!--Bootstrap’s tooltips require Popper-->
     <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
     <script type="text/javascript" src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>
+    <script type="text/javascript">
+        window.STATIC_URL = '{% static '' %}';
+    </script>
     <script type="text/javascript" src="{% static 'common/js/utils.js' %}"></script>
     <script type="text/javascript" src="{% static 'common/js/init.js' %}"></script>
     <!-- translations: https://docs.djangoproject.com/en/2.2/topics/i18n/translation/#using-the-javascript-translation-catalog -->
@@ -60,9 +63,31 @@
                     {% trans "Statystyki" %}
                     </a>
                 </li>
+                {% if perms.users.view_user %}
+                    <li class="nav-item" id="nav-users">
+                        <a class="nav-link text-light" href="{% url 'users:user_list' %}">
+                        {% trans "Użytkownicy" %}
+                        </a>
+                    </li>
+                {% endif %}
             </ul>
         </div>
         <span id="import-status" class="navbar-text text-warning mr-3"></span>
+        {% if request.user.is_authenticated %}
+            <div class="dropdown mr-3">
+                <a href="#" class="btn btn-sm btn-outline-light dropdown-toggle" data-toggle="dropdown">{{ request.user.get_full_name|default:request.user.username }}</a>
+                <div class="dropdown-menu dropdown-menu-right">
+                    <a href="{% url 'users:user_profile' %}" class="dropdown-item font-weight-bold text-dark text-uppercase">
+                        {% trans "Twój profil" %}
+                    </a>
+                    <a href="{% url 'users:logout' %}" class="dropdown-item font-weight-bold text-dark text-uppercase">
+                        {% trans "Wyloguj siÄ™" %}
+                    </a>
+                </div>
+            </div>
+        {% else %}
+            <a id="login-btn" class="btn btn-sm btn-outline-light mr-3" href="{% url 'users:login' %}">{% trans "Zaloguj siÄ™" %}</a>
+        {% endif %}
         <a
             id="lang-btn"
             class="btn btn-sm btn-outline-light"
diff --git a/dictionary_statistics/templates/dictionary_statistics.html b/dictionary_statistics/templates/dictionary_statistics.html
index 0b9d3662929e74a6ec12378cd5a5b7c6f1852272..9736e81c4a8d57b2e61825c9ba85eff93873437b 100644
--- a/dictionary_statistics/templates/dictionary_statistics.html
+++ b/dictionary_statistics/templates/dictionary_statistics.html
@@ -47,7 +47,7 @@
                 <tr>
                     {% for opinion, opinion_key, n in schema_stats %}
                         <th style="width: 10em;" scope="col">
-                            {% if opinion_key != "all" %}<img src="/static/entries/img/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
+                            {% if opinion_key != "all" %}<img src="{% static 'entries/img' %}/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
                         </th>
                     {% endfor %}
                 </tr>
@@ -71,7 +71,7 @@
                 <tr>
                     {% for opinion, opinion_key, n in frame_stats %}
                         <th scope="col">
-                            {% if opinion_key != "all" %}<img src="/static/entries/img/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
+                            {% if opinion_key != "all" %}<img src="{% static 'entries/img' %}/{{ opinion_key }}.svg" width="12" height="12" alt="{{ opinion }}"> {% endif %}{{ opinion }}
                         </th>
                     {% endfor %}
                 </tr>
diff --git a/entries/forms.py b/entries/forms.py
index c104bad09d1121af2e79bfddfcd1fc92f2fa0ba2..c30edf0c5d087d417b904ac626bff84412c2e21d 100644
--- a/entries/forms.py
+++ b/entries/forms.py
@@ -1,7 +1,7 @@
 from random import randint
 
 from django import forms
-
+from django.conf import settings
 from django.db.models import OneToOneField, ForeignKey, CharField, ManyToManyField, Q
 
 from django.utils.text import format_lazy
@@ -118,7 +118,7 @@ def and_or_form_creator(button_label, button_id, field=None, data_add=None, add_
     if help:
         help_tooltip = ''
         if tooltip:
-            help_tooltip = ' <span data-toggle="tooltip" data-placement="bottom" title="{}"><img src="/static/common/img/info.svg" alt="info" width="12" height="12"/></span>'.format(tooltip)
+            help_tooltip = f' <span data-toggle="tooltip" data-placement="bottom" title="{tooltip}"><img src="{settings.STATIC_URL}common/img/info.svg" alt="info" width="12" height="12"/></span>'
         ret.insert(-1, layout.HTML('<small class="form-text text-muted">{}{}</small>'.format(help, help_tooltip)))
     return ret
 
diff --git a/entries/static/entries/css/entries.css b/entries/static/entries/css/entries.css
index 17b7bea386094bab5a5189e82f9a66cf7f0e3de4..4a6d18cce28d3651a29d417a6e4fbc87c7dffb2b 100644
--- a/entries/static/entries/css/entries.css
+++ b/entries/static/entries/css/entries.css
@@ -156,7 +156,7 @@ legend {
 }
 
 .negated {
-    background-image: url("/static/entries/img/negated.png");
+    background-image: url("../img/negated.png");
     background-repeat: repeat;
 }
 
@@ -170,7 +170,7 @@ legend {
 }
 
 .frame.highlight .lemma, .example-role .lemma, .frame.active .lemma, .phrase.lemma {
-    background-image: url("/static/entries/img/lemma.png");
+    background-image: url("../img/lemma.png");
     background-repeat: repeat;
 }
 
@@ -196,12 +196,12 @@ legend {
 }
 
 .gutter.gutter-horizontal {
-    background-image: url("/static/entries/img/gutter-h.png");
+    background-image: url("../img/gutter-h.png");
     cursor: col-resize;
 }
 
 .gutter.gutter-vertical {
-    background-image: url("/static/entries/img/gutter-v.png");
+    background-image: url("../img/gutter-v.png");
     cursor: row-resize;
 }
 
diff --git a/entries/static/entries/js/entries.js b/entries/static/entries/js/entries.js
index 7230452406b7371151bed20be6671257184649ea..ce013f844713dd648d7e59953e72f9588b11815b 100644
--- a/entries/static/entries/js/entries.js
+++ b/entries/static/entries/js/entries.js
@@ -13,7 +13,7 @@ function make_opinion_row(item, span, width) {
     var opinion_row = document.createElement('tr');
     opinion_row.className = 'opinion-row';
     opinion_row.innerHTML = '<th scope="row" class="py-2 px-1 text-secondary" style="width: ' + width + 'em;">' + gettext('Opinia') + '</td>';
-    opinion_row.innerHTML += '<td class="opinion-cell py-2 px-1" colspan="' + span + '"><img src="/static/entries/img/' + item.opinion_key + '.svg" width="12" height="12" alt="' + item.opinion + '"> ' + item.opinion + '</td>';
+    opinion_row.innerHTML += '<td class="opinion-cell py-2 px-1" colspan="' + span + '"><img src="' + window.STATIC_URL + 'entries/img/' + item.opinion_key + '.svg" width="12" height="12" alt="' + item.opinion + '"> ' + item.opinion + '</td>';
     return opinion_row;
 }
 
@@ -126,9 +126,9 @@ function frame2dom(frame) {
             lu_html += ' ' + tooltipped_info('<i>' + tooltip.join('; ') + '</i>');
         }
         if (lu.url) {
-            //lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="/static/common/img/plwn.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
-            //lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="/static/common/img/ext-link.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
-            lu_html += ' <a class="lu-plwn" href="' + lu.url + '" target="_blank"><img src="/static/common/img/ext-link.svg" alt="external link" height="14"/></a>';
+//            lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="' + window.STATIC_URL + 'common/img/plwn.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
+//            lu_html += ' ' + tooltipped_span('<a href="' + lu.url + '" target="_blank"><img src="' + window.STATIC_URL + 'common/img/ext-link.svg" alt="external link" height="14"/></a>', gettext('Przejdź do strony tej jednostki w <i>Słowosieci</i>'), 'plwn-url');
+            lu_html += ' <a class="lu-plwn" href="' + lu.url + '" target="_blank"><img src="' + window.STATIC_URL + '/common/img/ext-link.svg" alt="external link" height="14"/></a>';
         }
         lexical_units.push(lu_html);
     }
@@ -163,7 +163,7 @@ function frame2dom(frame) {
             }
             preferences_html += '<div class="preference py-2 px-1' + cls + '">';
             if (preference.url) {
-                //preferences_html += ' <a class="synset-plwn" href="' + preference.url + '" target="_blank"><img src="/static/common/img/ext-link.svg" alt="external link" height="14"/></a>';
+//                preferences_html += ' <a class="synset-plwn" href="' + preference.url + '" target="_blank"><img src="' + window.STATIC_URL + 'common/img/ext-link.svg" alt="external link" height="14"/></a>';
                 preferences_html += ' <a class="synset-plwn" href="' + preference.url + '" target="_blank">' + preference.str + '</a>';
             } else {
                 preferences_html += preference.str;
diff --git a/entries/templates/checkboxselectmultiple_with_tooltips.html b/entries/templates/checkboxselectmultiple_with_tooltips.html
index 0ee0c68da9d953d46b00614ea29ca1c6cbd48a8d..b0cbaef385728f68dc73fa99153a4c99b9c62c7b 100644
--- a/entries/templates/checkboxselectmultiple_with_tooltips.html
+++ b/entries/templates/checkboxselectmultiple_with_tooltips.html
@@ -1,5 +1,6 @@
 {% load crispy_forms_filters %}
 {% load l10n %}
+{% load static %}
 
 <div class="{% if field_class %} {{ field_class }}{% endif %}"{% if flat_attrs %} {{ flat_attrs|safe }}{% endif %}>
 
@@ -8,7 +9,7 @@
         <input type="checkbox" class="{%if use_custom_control%}custom-control-input{% else %}form-check-input{% endif %}{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0 in field.value or choice.0|stringformat:"s" in field.value or choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.html_name }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
         <label class="{%if use_custom_control%}custom-control-label{% else %}form-check-label{% endif %}" for="id_{{ field.html_name }}_{{ forloop.counter }}">
             {% if choice.1.1 %}
-            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="/static/common/img/info.svg" alt="info" width="14" height="14"/></span>
+            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="14" height="14"/></span>
             {% else %}
             {{ choice.1.0|unlocalize }}
             {% endif %}
diff --git a/entries/templates/entries.html b/entries/templates/entries.html
index 0814f1f43ba66887f40b6ae0d9737bae6f5e7064..674d39f56e74127a3feea3b78d9cd0c209f63077 100644
--- a/entries/templates/entries.html
+++ b/entries/templates/entries.html
@@ -63,13 +63,13 @@
         <div class="form-check custom-control custom-checkbox">
             <input type="checkbox" class="custom-control-input" id="show-realisation-descriptions"{% if request.session.show_reals_desc %} checked{% endif %}>
             <label class="custom-control-label" for="show-realisation-descriptions">
-            {% trans "Wyświetlaj opisy realizacji" %} <span data-toggle="tooltip" data-placement="bottom" title="{% trans "Po wybraniu ramy wyświetlaj opisy jej realizacji składniowych. Opisy (dla całej realizacji i dla poszczególnych fraz) są wyświetlane wewnątrz schematów." %}"><img src="/static/common/img/info.svg" alt="info" width="10" height="10"/></span>
+            {% trans "Wyświetlaj opisy realizacji" %} <span data-toggle="tooltip" data-placement="bottom" title="{% trans "Po wybraniu ramy wyświetlaj opisy jej realizacji składniowych. Opisy (dla całej realizacji i dla poszczególnych fraz) są wyświetlane wewnątrz schematów." %}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="10" height="10"/></span>
             </label>
         </div>
         <div class="form-check custom-control custom-checkbox">
             <input type="checkbox" class="custom-control-input" id="show-linked-entries"{% if request.session.show_linked_entries %} checked{% endif %}>
             <label class="custom-control-label" for="show-linked-entries">
-            {% trans "Wyświetlaj powiązane hasła" %} <span data-toggle="tooltip" data-placement="bottom" data-html="true" title="{% trans "Przy filtrowaniu haseł wyświetlaj, oprócz haseł spełniających kryteria filtrowania, hasła powiązane z nimi znaczeniowo (np. <i>podarować</i> – <i>podarunek</i> – <i>podarek</i>). Hasła powiązane niespełniające kryteriów filtrowania są wyróżnione jaśniejszym kolorem na liście oraz nie podlegają filtrowaniu schematów i ram (są zawsze wyświetlane w całości niezależnie od użytych filtrów dla schematów/ram)." %}"><img src="/static/common/img/info.svg" alt="info" width="10" height="10"/></span>
+            {% trans "Wyświetlaj powiązane hasła" %} <span data-toggle="tooltip" data-placement="bottom" data-html="true" title="{% trans "Przy filtrowaniu haseł wyświetlaj, oprócz haseł spełniających kryteria filtrowania, hasła powiązane z nimi znaczeniowo (np. <i>podarować</i> – <i>podarunek</i> – <i>podarek</i>). Hasła powiązane niespełniające kryteriów filtrowania są wyróżnione jaśniejszym kolorem na liście oraz nie podlegają filtrowaniu schematów i ram (są zawsze wyświetlane w całości niezależnie od użytych filtrów dla schematów/ram)." %}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="10" height="10"/></span>
             </label>
         </div>
     </div>
diff --git a/entries/templates/role_checkboxselectmultiple_inner.html b/entries/templates/role_checkboxselectmultiple_inner.html
index 23a00fe0d4f1e935c052042df6363d0e58829afc..6310fe05320954dfef444387b26231e5f5ba0308 100644
--- a/entries/templates/role_checkboxselectmultiple_inner.html
+++ b/entries/templates/role_checkboxselectmultiple_inner.html
@@ -26,7 +26,7 @@
         <input type="checkbox" class="{%if use_custom_control%}custom-control-input{% else %}form-check-input{% endif %}{%if is_bound %} is-{% if field.errors %}in{%endif%}valid{% endif %}"{% if choice.0 in field.value or choice.0|stringformat:"s" in field.value or choice.0|stringformat:"s" == field.value|default_if_none:""|stringformat:"s" %} checked="checked"{% endif %} name="{{ field.html_name }}" id="id_{{ field.html_name }}_{{ forloop.parentloop.parentloop.counter }}_{{ forloop.parentloop.counter }}_{{ forloop.counter }}" value="{{ choice.0|unlocalize }}" {{ field.field.widget.attrs|flatatt }}>
         <label class="{%if use_custom_control%}custom-control-label{% else %}form-check-label{% endif %} text-dark" for="id_{{ field.html_name }}_{{ forloop.parentloop.parentloop.counter }}_{{ forloop.parentloop.counter }}_{{ forloop.counter }}">
             {% if choice.1.1 %}
-            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="/static/common/img/info.svg" alt="info" width="14" height="14"/></span>
+            {{ choice.1.0|unlocalize }} <span data-toggle="tooltip" data-placement="bottom" title="{{ choice.1.1 }}"><img src="{% static 'common/img/info.svg' %}" alt="info" width="14" height="14"/></span>
             {% else %}
             {{ choice.1.0|unlocalize }}
             {% endif %}
diff --git a/entries/templates/test.html b/entries/templates/test.html
index 7f838aae952aea64d4f99b6b0c28892b7786b668..c3c96aaf6ff5973fc2d3e7260cd688dc917ea496 100644
--- a/entries/templates/test.html
+++ b/entries/templates/test.html
@@ -8,7 +8,7 @@
 <head>
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
-    <link rel="icon" href="/static/common/favicon.ico">
+    <link rel="icon" href="{% static 'common/favicon.ico' %}">
     <link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/smoothness/jquery-ui.css">
     <link rel="stylesheet" type="text/css" href="https://bootswatch.com/4/lux/bootstrap.min.css">
     <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
diff --git a/filters/static/filters/js/filters_test.js b/filters/static/filters/js/filters_test.js
index 43f45779829d7db5bcab954b1e190b14a6bd3f21..c0fa13bf9eca4a52df765554c5524ebde9d6a7d4 100644
--- a/filters/static/filters/js/filters_test.js
+++ b/filters/static/filters/js/filters_test.js
@@ -34,14 +34,14 @@ function show_spinner() {
     clear_info();
     // silly cat gifs for development
     /*var rnd = Math.floor(Math.random() * 3) + 1;
-    show_status('<div class="text-center mb-1"><p>Siem filtruje!</p><img src="/static/common/img/loading' + rnd + '.gif" height=180px></div>');*/
+    show_status('<div class="text-center mb-1"><p>Siem filtruje!</p><img src="' + window.STATIC_URL + 'common/img/loading' + rnd + '.gif" height=180px></div>');*/
     show_status('<div class="text-center"><div class="spinner-grow text-light" role="status"> <span class="sr-only">Proszę czekać, trwa filtrowanie...</span></div></div>');
 }
 
 function show_error() {
     clear_info();
     // silly cat gif for development
-    /*show_status('<div class="text-center mb-1"><p>Ajajaj, straszny błąd!</p><img src="/static/common/img/error.gif" height="320px"></div>');*/
+    /*show_status('<div class="text-center mb-1"><p>Ajajaj, straszny błąd!</p><img src="' + window.STATIC_URL + 'common/img/error.gif" height="320px"></div>');*/
     show_warning_text('Coś poszło nie tak... :(');
     
 }
diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo
index 63b3175bf7c12ea66fc505d606cae8959df2877b..f7595614722432da63fc8ccf1e3efcd5f783fe8f 100644
Binary files a/locale/en/LC_MESSAGES/django.mo and b/locale/en/LC_MESSAGES/django.mo differ
diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 020256e4bf0b2c973e67271a7cb0cdaa17e066ed..56413e06d44ffb05795ec4e8352700e97c68a701 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-07-14 16:24+0200\n"
+"POT-Creation-Date: 2022-04-07 23:17+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,49 +18,69 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: common/templates/base.html:49 entries/templates/entries.html:7
+#: common/templates/base.html:52 entries/templates/entries.html:7
 #: entries/templates/entries.html:38
 msgid "Hasła"
 msgstr "Entries"
 
-#: common/templates/base.html:55
+#: common/templates/base.html:58
 msgid "Typy fraz"
 msgstr "Phrase types"
 
-#: common/templates/base.html:60
+#: common/templates/base.html:63
 #: dictionary_statistics/templates/dictionary_statistics.html:9
 msgid "Statystyki"
 msgstr "Statistics"
 
-#: common/templates/base.html:71
+#: common/templates/base.html:69 users/templates/user_list.html:5
+#: users/templates/user_list.html:9
+msgid "Użytkownicy"
+msgstr "Users"
+
+#: common/templates/base.html:81 users/templates/user_profile.html:7
+#: users/templates/user_profile.html:10
+msgid "Twój profil"
+msgstr "Your profile"
+
+#: common/templates/base.html:84
+msgid "Wyloguj siÄ™"
+msgstr "Sign out"
+
+#: common/templates/base.html:89 users/forms.py:71
+#: users/templates/registration/login.html:6
+#: users/templates/registration/login.html:12
+msgid "Zaloguj siÄ™"
+msgstr "Sign in"
+
+#: common/templates/base.html:96
 msgid "EN"
 msgstr "PL"
 
-#: common/templates/base.html:81
+#: common/templates/base.html:106
 msgid "Instytut Podstaw Informatyki PAN"
 msgstr "Institute of Computer Science PAS"
 
-#: common/templates/base.html:82
+#: common/templates/base.html:107
 msgid "Praca współfinansowana przez"
 msgstr "Work co-founded by"
 
-#: common/templates/base.html:83
+#: common/templates/base.html:108
 msgid "Strona wykorzystuje"
 msgstr "Webpage powered by"
 
-#: common/templates/base.html:84
+#: common/templates/base.html:109
 msgid "oraz"
 msgstr "and "
 
-#: common/templates/base.html:85
+#: common/templates/base.html:110
 msgid "z motywem opartym na"
 msgstr "with"
 
-#: common/templates/base.html:86
+#: common/templates/base.html:111
 msgid " i krojem pisma"
 msgstr "-based motive and"
 
-#: common/templates/base.html:87
+#: common/templates/base.html:112
 msgid "."
 msgstr " font."
 
@@ -122,7 +142,7 @@ msgstr "all"
 msgid "Pobieranie"
 msgstr "Download"
 
-#: entries/autocompletes.py:27 entries/views.py:464
+#: entries/autocompletes.py:27 entries/views.py:463
 msgid "definicja:"
 msgstr "definition:"
 
@@ -250,7 +270,7 @@ msgstr "Schema"
 msgid "Schemat(y) występujące w haśle"
 msgstr "Phrase type(s) occurring in the entry."
 
-#: entries/forms.py:147 entries/forms.py:422 entries/forms.py:643
+#: entries/forms.py:147 entries/forms.py:422
 msgid "Pozycja"
 msgstr "Position"
 
@@ -268,9 +288,9 @@ msgstr ""
 "positions co-occuring in one schema, use the POSITION filter inside SCHEMA "
 "filter above."
 
-#: entries/forms.py:151 entries/forms.py:483 entries/forms.py:648
-#: entries/forms.py:769 entries/forms.py:771 entries/forms.py:773
-#: entries/forms.py:775
+#: entries/forms.py:151 entries/forms.py:483 entries/forms.py:649
+#: entries/forms.py:770 entries/forms.py:772 entries/forms.py:774
+#: entries/forms.py:776
 msgid "Fraza"
 msgstr "Phrase"
 
@@ -296,7 +316,7 @@ msgstr "Frame"
 msgid "Rama/y występujące w haśle"
 msgstr "Frame(s) occurring in the entry."
 
-#: entries/forms.py:165 entries/forms.py:600
+#: entries/forms.py:165 entries/forms.py:593
 msgid "Argument"
 msgstr "Argument"
 
@@ -349,7 +369,7 @@ msgstr "Collapse"
 msgid "Usuń"
 msgstr "Remove"
 
-#: entries/forms.py:311 entries/forms.py:807
+#: entries/forms.py:311 entries/forms.py:808
 msgid "Zaneguj"
 msgstr "Negate"
 
@@ -433,7 +453,7 @@ msgstr "Lemma choice"
 msgid "Łączenie lematów"
 msgstr "Lemma joining"
 
-#: entries/forms.py:548 entries/forms.py:828
+#: entries/forms.py:548 entries/forms.py:829
 msgid "Typ składniowy frazy zleksykalizowanej."
 msgstr "Syntactic type of lexicalised phrase."
 
@@ -451,22 +471,22 @@ msgstr "Opinion"
 msgid "Liczba argumentów"
 msgstr "Number of arguments"
 
-#: entries/forms.py:594
-msgid "Liczba preferencyj selekcyjnych argumentu"
-msgstr "Number of argument’s selectional preferences"
-
-#: entries/forms.py:616
+#: entries/forms.py:609
 msgid "Argument semantyczny"
 msgstr "Semantic argument"
 
-#: entries/forms.py:622
+#: entries/forms.py:615
 msgid "Rola"
 msgstr "Role"
 
-#: entries/forms.py:629
+#: entries/forms.py:622
 msgid "Atrybut roli"
 msgstr "Role attribute"
 
+#: entries/forms.py:629
+msgid "Liczba preferencyj selekcyjnych argumentu"
+msgstr "Number of argument’s selectional preferences"
+
 #: entries/forms.py:636 entries/forms.py:639
 msgid "Preferencja selekcyjna"
 msgstr "Selectional preference"
@@ -483,59 +503,59 @@ msgstr "Expressed by relation"
 msgid "Wyrażona przez jednostkę leksykalną Słowosieci"
 msgstr "Expressed by plWordnet lexical unit"
 
-#: entries/forms.py:647
+#: entries/forms.py:648
 msgid "Typ frazy, przez którą może być realizowany argument."
 msgstr ""
 
-#: entries/forms.py:670
+#: entries/forms.py:671
 msgid "Preferencja predefiniowana"
 msgstr "Predefined preference"
 
-#: entries/forms.py:676
+#: entries/forms.py:677
 msgid "Predefiniowane"
 msgstr "Predefined"
 
-#: entries/forms.py:690
+#: entries/forms.py:691
 msgid "Preferencja – relacja"
 msgstr "Relational preference"
 
-#: entries/forms.py:696
+#: entries/forms.py:697
 msgid "Relacja"
 msgstr "Relation"
 
-#: entries/forms.py:707
+#: entries/forms.py:708
 msgid "Do: rola"
 msgstr "To: role"
 
-#: entries/forms.py:714
+#: entries/forms.py:715
 msgid "Do: atrybut"
 msgstr "To: attribute"
 
-#: entries/forms.py:726
+#: entries/forms.py:727
 msgid "Preferencja – Słowosieć"
 msgstr "plWordnet preference"
 
-#: entries/forms.py:732
+#: entries/forms.py:733
 msgid "Jednostka leksykalna"
 msgstr "Lexical unit"
 
-#: entries/forms.py:780
+#: entries/forms.py:781
 msgid "Fraza {}"
 msgstr "{} phrase"
 
-#: entries/forms.py:782 entries/phrase_descriptions/descriptions.py:124
+#: entries/forms.py:783 entries/phrase_descriptions/descriptions.py:124
 msgid "zleksykalizowana"
 msgstr "lexicalised"
 
-#: entries/forms.py:818
+#: entries/forms.py:819
 msgid "Realizacja składniowa frazy."
 msgstr "Syntactic realisation of the phrase."
 
-#: entries/forms.py:822
+#: entries/forms.py:823
 msgid "Fraza składowa zleksykalizowanej konstrukcji porównawczej."
 msgstr "Component phrase of lexicalised comparative construction."
 
-#: entries/forms.py:824
+#: entries/forms.py:825
 msgid "Fraza zleksykalizowana."
 msgstr "Lexicalised phrase."
 
@@ -2157,7 +2177,13 @@ msgid ""
 "filtrowania są wyróżnione jaśniejszym kolorem na liście oraz nie podlegają "
 "filtrowaniu schematów i ram (są zawsze wyświetlane w całości niezależnie od "
 "użytych filtrów dla schematów/ram)."
-msgstr "When filtering entries, show (aside from entries satisfying filtering criteria) entries with related meanings, (eg. <i>podarować</i> – <i>podarunek</i> – <i>podarek</i>). Related entries that don’t satisfy filtering criteria are displayed in a lighter color on the entries list and are not subject to schema/frame filtering (ie. are always shown in their entirety regardless of schema/frame filters applied)."
+msgstr ""
+"When filtering entries, show (aside from entries satisfying filtering "
+"criteria) entries with related meanings, (eg. <i>podarować</i> – "
+"<i>podarunek</i> – <i>podarek</i>). Related entries that don’t satisfy "
+"filtering criteria are displayed in a lighter color on the entries list and "
+"are not subject to schema/frame filtering (ie. are always shown in their "
+"entirety regardless of schema/frame filters applied)."
 
 #: entries/templates/entries.html:103
 msgid "Filtrowanie haseł"
@@ -2205,22 +2231,22 @@ msgstr "Source"
 msgid "Brak przykładów"
 msgstr "No examples"
 
-#: entries/views.py:452
+#: entries/views.py:451
 msgid ""
 "Realizacja tego argumentu w zdaniu powinna być powiązana jakąkolwiek relacją"
 msgstr "Realisation of this argument in the sentence should be in any relation"
 
-#: entries/views.py:454
+#: entries/views.py:453
 msgid ""
 "Realizacja tego argumentu w zdaniu powinna być powiązana relacją <i>{}</i>"
 msgstr ""
 "Realisation of this argument in the sentence should be in <i>{}</i> relation"
 
-#: entries/views.py:455
+#: entries/views.py:454
 msgid "z realizacjÄ… argumentu <i>{}</i>."
 msgstr "with realisation of the <i>{}</i> argument."
 
-#: entries/views.py:468
+#: entries/views.py:467
 msgid "hiperonimy:"
 msgstr "hypernyms"
 
@@ -2264,6 +2290,55 @@ msgstr "distributive phrase"
 msgid "fraza posesywna"
 msgstr "possesive phrase"
 
+#: users/forms.py:10 users/templates/user_list.html:21
+msgid "Grupa"
+msgstr "Group"
+
+#: users/forms.py:29 users/forms.py:59
+msgid "Zapisz"
+msgstr "Save"
+
+#: users/forms.py:30 users/forms.py:60
+msgid "Wróć"
+msgstr "Back"
+
+#: users/forms.py:84
+msgid "Zresetuj hasło"
+msgstr "Reset password"
+
+#: users/templates/registration/password_reset.html:6
+#: users/templates/registration/password_reset.html:12
+msgid "Zresetuj swoje hasło"
+msgstr "Reset your password"
+
+#: users/templates/user_list.html:12 users/views.py:35
+msgid "Dodaj użytkownika"
+msgstr "Add a user"
+
+#: users/templates/user_list.html:19
+msgid "ImiÄ™ i nazwisko"
+msgstr "Full name"
+
+#: users/templates/user_list.html:20
+msgid "Nazwa użytkownika"
+msgstr "Username"
+
+#: users/templates/user_list.html:22
+msgid "Aktywny"
+msgstr "Active"
+
+#: users/templates/user_list.html:23
+msgid "Akcje"
+msgstr "Actions"
+
+#: users/templates/user_list.html:33
+msgid "Edytuj"
+msgstr "Edit"
+
+#: users/views.py:48
+msgid "Edytuj użytkownika"
+msgstr "Edit user"
+
 #~ msgid "niepewna"
 #~ msgstr "uncertain"
 
diff --git a/locale/en/LC_MESSAGES/djangojs.po b/locale/en/LC_MESSAGES/djangojs.po
index 43fea1062dec5b7425e2404f4a6c74e3ece07c9c..fb7fa6fc0cacfb9be3b7ebf56bdaeacf3a7298e4 100644
--- a/locale/en/LC_MESSAGES/djangojs.po
+++ b/locale/en/LC_MESSAGES/djangojs.po
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2021-07-14 16:24+0200\n"
+"POT-Creation-Date: 2022-04-07 23:17+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -22,157 +22,157 @@ msgstr ""
 msgid "Trwa import danych!"
 msgstr "Data import in progress!"
 
-#: entries/static/entries/js/entries.js:14
+#: entries/static/entries/js/entries.js:15
 msgid "Opinia"
 msgstr "Opinion"
 
-#: entries/static/entries/js/entries.js:33
+#: entries/static/entries/js/entries.js:34
 msgid "Funkcja"
 msgstr "Function"
 
-#: entries/static/entries/js/entries.js:36
+#: entries/static/entries/js/entries.js:37
 msgid "Typy fraz"
 msgstr "Phrase types"
 
-#: entries/static/entries/js/entries.js:97
+#: entries/static/entries/js/entries.js:98
 msgid "brak schematów"
 msgstr "no schemata"
 
-#: entries/static/entries/js/entries.js:122
+#: entries/static/entries/js/entries.js:123
 msgid "nowa jednostka spoza <i>SÅ‚owosieci</i>"
 msgstr "new lexical unit not in <i>plWordnet</i>"
 
-#: entries/static/entries/js/entries.js:146
+#: entries/static/entries/js/entries.js:147
 msgid "Rola"
 msgstr "Role"
 
-#: entries/static/entries/js/entries.js:148
+#: entries/static/entries/js/entries.js:149
 msgid "Preferencje selekcyjne"
 msgstr "Selectional preferences"
 
-#: entries/static/entries/js/entries.js:195
+#: entries/static/entries/js/entries.js:196
 msgid "brak ram"
 msgstr "no frames"
 
-#: entries/static/entries/js/entries.js:213
+#: entries/static/entries/js/entries.js:214
 msgid "Kliknij, aby wyświetlić przykłady dla tego schematu."
 msgstr "Click to show examples for this schema."
 
-#: entries/static/entries/js/entries.js:250
+#: entries/static/entries/js/entries.js:251
 msgid "Kliknij, aby cofnąć wyświetlanie przykładów dla tego schematu."
 msgstr "Click to undo showing examples for this schema."
 
-#: entries/static/entries/js/entries.js:262
-#: entries/static/entries/js/entries.js:453
-#: entries/static/entries/js/entries.js:485
-#: entries/static/entries/js/entries.js:531
+#: entries/static/entries/js/entries.js:263
+#: entries/static/entries/js/entries.js:454
+#: entries/static/entries/js/entries.js:486
+#: entries/static/entries/js/entries.js:532
 msgid ""
 "Kliknij, aby cofnąć ograniczenie wyświetlanych przykładów do powiązanych z"
 msgstr "Click to undo restriction to examples linked to"
 
-#: entries/static/entries/js/entries.js:262
-#: entries/static/entries/js/entries.js:264
+#: entries/static/entries/js/entries.js:263
+#: entries/static/entries/js/entries.js:265
 msgid "tÄ… pozycjÄ…"
 msgstr "this position"
 
-#: entries/static/entries/js/entries.js:262
-#: entries/static/entries/js/entries.js:264
+#: entries/static/entries/js/entries.js:263
+#: entries/static/entries/js/entries.js:265
 msgid "tÄ… frazÄ…"
 msgstr "this phrase"
 
-#: entries/static/entries/js/entries.js:264
-#: entries/static/entries/js/entries.js:455
-#: entries/static/entries/js/entries.js:487
-#: entries/static/entries/js/entries.js:533
+#: entries/static/entries/js/entries.js:265
+#: entries/static/entries/js/entries.js:456
+#: entries/static/entries/js/entries.js:488
+#: entries/static/entries/js/entries.js:534
 msgid "Kliknij, aby wyświetlić wyłącznie przykłady powiązane z"
 msgstr "Click to show only examples linked to"
 
-#: entries/static/entries/js/entries.js:350
+#: entries/static/entries/js/entries.js:351
 msgid ""
 "Kliknij, aby wyświetlić przykłady dla tej ramy oraz jej realizacje "
 "składniowe."
 msgstr ""
 "Click to show examples linked to this frame and its syntactic realisations."
 
-#: entries/static/entries/js/entries.js:453
-#: entries/static/entries/js/entries.js:455
+#: entries/static/entries/js/entries.js:454
+#: entries/static/entries/js/entries.js:456
 msgid "tym znaczeniem"
 msgstr "this meaning"
 
-#: entries/static/entries/js/entries.js:485
-#: entries/static/entries/js/entries.js:487
+#: entries/static/entries/js/entries.js:486
+#: entries/static/entries/js/entries.js:488
 msgid "tÄ… rolÄ…"
 msgstr "this role"
 
-#: entries/static/entries/js/entries.js:531
-#: entries/static/entries/js/entries.js:533
+#: entries/static/entries/js/entries.js:532
+#: entries/static/entries/js/entries.js:534
 msgid "tym schematem"
 msgstr "this schema"
 
-#: entries/static/entries/js/entries.js:561
+#: entries/static/entries/js/entries.js:562
 msgid "Kliknij, aby cofnąć wybór tej ramy."
 msgstr "Click to undo choice if this frame."
 
-#: entries/static/entries/js/entries.js:664
-#: entries/static/entries/js/entries.js:770
+#: entries/static/entries/js/entries.js:665
+#: entries/static/entries/js/entries.js:771
 msgid "Komentarz"
 msgstr "Comment"
 
-#: entries/static/entries/js/entries.js:681
+#: entries/static/entries/js/entries.js:682
 msgid ""
 "Kliknij, aby cofnąć wyświetlanie typów fraz powiązanych z tym przykładem."
 msgstr "Click to undo showing phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:683
+#: entries/static/entries/js/entries.js:684
 msgid "Kliknij, aby wyświetlić typy fraz powiązane z tym przykładem."
 msgstr "Click to show phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:722
+#: entries/static/entries/js/entries.js:723
 msgid ""
 "Kliknij, aby cofnąć wyświetlanie argumentów i typów fraz powiązanych z tym "
 "przykładem."
 msgstr ""
 "Click to undo showing arguments and phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:724
+#: entries/static/entries/js/entries.js:725
 msgid ""
 "Kliknij, aby wyświetlić argumenty i typy fraz powiązane z tym przykładem."
 msgstr "Click to show arguments and phrase types linked to this example."
 
-#: entries/static/entries/js/entries.js:981
+#: entries/static/entries/js/entries.js:984
 msgid "Przetwarzanie..."
 msgstr "Processing"
 
-#: entries/static/entries/js/entries.js:982
-#: entries/static/entries/js/entries.js:1039
+#: entries/static/entries/js/entries.js:985
+#: entries/static/entries/js/entries.js:1049
 msgid "Szukaj:"
 msgstr "Search:"
 
-#: entries/static/entries/js/entries.js:983
+#: entries/static/entries/js/entries.js:986
 msgid "Liczba haseł: _TOTAL_"
 msgstr "_TOTAL_ entries"
 
-#: entries/static/entries/js/entries.js:984
-#: entries/static/entries/js/entries.js:1040
+#: entries/static/entries/js/entries.js:987
+#: entries/static/entries/js/entries.js:1050
 msgid "Liczba haseł: 0"
 msgstr "0 entries"
 
-#: entries/static/entries/js/entries.js:985
+#: entries/static/entries/js/entries.js:988
 msgid "(spośród _MAX_)"
 msgstr "(out of _MAX_)"
 
-#: entries/static/entries/js/entries.js:986
-#: entries/static/entries/js/entries.js:1041
+#: entries/static/entries/js/entries.js:989
+#: entries/static/entries/js/entries.js:1051
 msgid "Brak haseł do wyświetlenia."
 msgstr "No entries to display."
 
-#: entries/static/entries/js/entries.js:988
-#: entries/static/entries/js/entries.js:1043
+#: entries/static/entries/js/entries.js:991
+#: entries/static/entries/js/entries.js:1053
 msgid ": sortuj kolumnÄ™ rosnÄ…co"
 msgstr ": sort column in ascending order"
 
-#: entries/static/entries/js/entries.js:989
-#: entries/static/entries/js/entries.js:1044
+#: entries/static/entries/js/entries.js:992
+#: entries/static/entries/js/entries.js:1054
 msgid ": sortuj kolumnÄ™ malejÄ…co"
 msgstr ": sort column in descending order"
 
diff --git a/phrase_expansions/templates/phrase_expansions.html b/phrase_expansions/templates/phrase_expansions.html
index c5242aebc46dda1c93cc2bf351777b623aba5d69..a975489d63511fd6fa2dcc7ab99bbae2b4b0ee3f 100644
--- a/phrase_expansions/templates/phrase_expansions.html
+++ b/phrase_expansions/templates/phrase_expansions.html
@@ -55,7 +55,7 @@
                                         {{ subtype_expansions.phrase_subtype }}
                                     </th>
                                     {% endif %}
-                                    <td class="py-2 px-1" style="width: 7em;"><img src="/static/entries/img/{{ expansion.opinion_sym }}.svg" alt="{{ expansion.opinion_str }}" width="12" height="12">
+                                    <td class="py-2 px-1" style="width: 7em;"><img src="{% static 'entries/img' %}/{{ expansion.opinion_sym }}.svg" alt="{{ expansion.opinion_str }}" width="12" height="12">
                                         {{ expansion.opinion_str }}
                                     </td>
                                     {% for position in expansion.positions %}
diff --git a/reset_db.sh b/reset_db.sh
index d4446d256020e1a1c28075709208ce33ba2d55c4..a90511373c2f276b92e9e472ab9d60cd9c0e75cd 100755
--- a/reset_db.sh
+++ b/reset_db.sh
@@ -13,6 +13,7 @@ python manage.py migrate
 
 rm import.log || true
 
+time python manage.py create_groups_and_permissions
 time python manage.py start_import
 time python manage.py import_expansions
 time python manage.py import_plWordnet
diff --git a/shellvalier/settings.py b/shellvalier/settings.py
index 797463aca63bea7d6a5c7e0579df919c03a1f0da..89ee949596a18b838e7573bf307d4a64a48b9cfc 100644
--- a/shellvalier/settings.py
+++ b/shellvalier/settings.py
@@ -11,6 +11,9 @@ https://docs.djangoproject.com/en/2.1/ref/settings/
 """
 
 import os
+import uuid
+
+from django.urls import reverse_lazy
 
 from .environment import get_environment, boolean_mapper, list_mapper_factory
 
@@ -58,6 +61,7 @@ INSTALLED_APPS = [
     'phrase_expansions.apps.PhraseExpansionsConfig',
     'dictionary_statistics.apps.DictionaryStatisticsConfig',
     'download.apps.DownloadConfig',
+    'users.apps.UsersConfig',
     'crispy_forms',
     'django_extensions',
 ]
@@ -151,9 +155,13 @@ LOCALE_PATHS = [
 # Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/2.1/howto/static-files/
 
-STATIC_URL = '/static/'
+VERSION = get_environment('VERSION', default=str(uuid.uuid4()))
+STATIC_URL = f'/static/{VERSION}/'
 
 STATICFILES_DIRS = [
     os.path.join(BASE_DIR, 'common/static/'),
 ]
 
+LOGIN_URL = reverse_lazy("users:login")
+LOGIN_REDIRECT_URL = reverse_lazy('dash')
+LOGOUT_REDIRECT_URL = reverse_lazy('dash')
diff --git a/shellvalier/urls.py b/shellvalier/urls.py
index d1178a8cbcafe0f184d611d7f7c739fbafea9440..1fa7a875c60dc8c90fddf7b461547362e17ce90b 100644
--- a/shellvalier/urls.py
+++ b/shellvalier/urls.py
@@ -13,6 +13,7 @@ urlpatterns = i18n_patterns(
     path('phrase_expansions/', include('phrase_expansions.urls')),
     path('dictionary_statistics/', include('dictionary_statistics.urls')),
     path('download/', include('download.urls')),
+    path('users/', include('users.urls')),
     path('jsi18n/', JavaScriptCatalog.as_view(), name='javascript-catalog'),
     path('admin/', admin.site.urls, name='admin'),
     path('', dash, name='dash'),
diff --git a/users/__init__.py b/users/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/users/apps.py b/users/apps.py
new file mode 100644
index 0000000000000000000000000000000000000000..4ce1fabc032a21e92bb0e0299a650bcdc4fffefe
--- /dev/null
+++ b/users/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class UsersConfig(AppConfig):
+    name = 'users'
diff --git a/users/forms.py b/users/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..e0d98f1683665d27456c77d4808a485879d91055
--- /dev/null
+++ b/users/forms.py
@@ -0,0 +1,87 @@
+from django import forms
+from django.contrib.auth.models import Group, User
+from django.utils.translation import gettext as _
+
+from crispy_forms.helper import FormHelper
+from crispy_forms.layout import HTML, Layout, Fieldset, ButtonHolder, Submit
+
+
+class UserForm(forms.ModelForm):
+    group = forms.ModelChoiceField(queryset=Group.objects.all(), label=_("Grupa"))
+
+    class Meta:
+        model = User
+        fields = [
+            'first_name',
+            'last_name',
+            'username',
+            'email',
+            'group',
+            'is_active',
+        ]
+
+    def __init__(self, *args, instance, **kwargs):
+        super().__init__(*args, instance=instance, **kwargs)
+        self.helper = FormHelper()
+        self.helper.layout = Layout(
+            Fieldset('', 'first_name', 'last_name', 'username', 'email', 'group', 'is_active'),
+            ButtonHolder(
+                Submit('submit', _('Zapisz'), css_class='btn btn-sm btn-success'),
+                HTML('''<a class="btn btn-sm btn-light" href="{% url 'users:user_list' %}">''' + _('Wróć') + '</a>'),
+            ),
+        )
+        for field in ['first_name', 'last_name', 'email']:
+            self.fields[field].required = True
+        if instance.pk:
+            self.initial['group'] = instance.groups.first()
+
+    def save(self, commit=True):
+        instance = super().save(commit=commit)
+        instance.groups.set([self.cleaned_data['group']])
+        return instance
+
+
+class UserProfileForm(forms.ModelForm):
+    class Meta:
+        model = User
+        fields = [
+            'first_name',
+            'last_name',
+            'email',
+        ]
+
+    def __init__(self, *args, instance, **kwargs):
+        super().__init__(*args, instance=instance, **kwargs)
+        self.helper = FormHelper()
+        self.helper.layout = Layout(
+            Fieldset('', 'first_name', 'last_name', 'email'),
+            ButtonHolder(
+                Submit('submit', _('Zapisz'), css_class='btn btn-sm btn-success'),
+                HTML('''<a class="btn btn-sm btn-light" href="{% url 'dash' %}">''' + _('Wróć') + '</a>'),
+            ),
+        )
+        for field in ['first_name', 'last_name', 'email']:
+            self.fields[field].required = True
+
+
+login_form_helper = FormHelper()
+login_form_helper.layout = Layout(
+    Fieldset('', 'username', 'password'),
+    ButtonHolder(
+        Submit('submit', _('Zaloguj siÄ™'), css_class='btn btn-sm btn-success'),
+        HTML(
+            '''<a class="btn btn-sm btn-light" href="{% url 'users:password_reset' %}">'''
+            f"{_('Nie pamiętam hasła')}"
+            '</a>'
+        ),
+    ),
+)
+
+password_reset_form_helper = FormHelper()
+password_reset_form_helper.layout = Layout(
+    Fieldset('', 'email'),
+    ButtonHolder(
+        Submit('submit', _('Zresetuj hasło'), css_class='btn btn-sm btn-success'),
+        HTML('''<a class="btn btn-sm btn-light" href="{% url 'users:login' %}">''' f"{_('Wróć')}" '</a>'),
+    )
+)
diff --git a/users/management/__init__.py b/users/management/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/users/management/commands/__init__.py b/users/management/commands/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/users/management/commands/create_groups_and_permissions.py b/users/management/commands/create_groups_and_permissions.py
new file mode 100644
index 0000000000000000000000000000000000000000..195d2d1942458bd7a63f4d41ff8c4a97c051dc5f
--- /dev/null
+++ b/users/management/commands/create_groups_and_permissions.py
@@ -0,0 +1,22 @@
+from django.contrib.auth.models import Group, Permission, User
+from django.contrib.contenttypes.models import ContentType
+from django.core.management.base import BaseCommand
+
+
+class Command(BaseCommand):
+    def handle(self, **options):
+        admins, __ = Group.objects.get_or_create(name="Admini")
+        admins.permissions.add(
+            Permission.objects.get(codename="view_user", content_type=ContentType.objects.get_for_model(User)),
+            Permission.objects.get(codename="add_user", content_type=ContentType.objects.get_for_model(User)),
+            Permission.objects.get(codename="change_user", content_type=ContentType.objects.get_for_model(User)),
+            Permission.objects.get(codename="delete_user", content_type=ContentType.objects.get_for_model(User)),
+        )
+        lexicographs, __ = Group.objects.get_or_create(name="Leksykografowie")
+        lexicographs.permissions.add(
+            # TODO
+        )
+        super_lexicographs, __ = Group.objects.get_or_create(name="Super Leksykografowie")
+        super_lexicographs.permissions.add(
+            # TODO
+        )
diff --git a/users/templates/registration/login.html b/users/templates/registration/login.html
new file mode 100644
index 0000000000000000000000000000000000000000..10834c7ffb611fb88384be4752384c3879a255c2
--- /dev/null
+++ b/users/templates/registration/login.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_filters %}
+
+{% block title %}{% trans "Zaloguj siÄ™" %}{% endblock %}
+
+{% block content %}
+    <div class="row m-0">
+        <div class="col-lg-4 col-md-6 m-auto">
+            <div class="bg-white mt-4 p-3 clearfix">
+                <h5 class="mt-2 mb-4">{% trans "Zaloguj siÄ™" %}</h5>
+                <form action="." method="post">
+                    {% crispy form helper %}
+                </form>
+            </div>
+        </div>
+    </div>
+{% endblock %}
diff --git a/users/templates/registration/password_reset.html b/users/templates/registration/password_reset.html
new file mode 100644
index 0000000000000000000000000000000000000000..7d37fa921a0e368d281916c3a87cd588213ea538
--- /dev/null
+++ b/users/templates/registration/password_reset.html
@@ -0,0 +1,19 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+{% load crispy_forms_filters %}
+
+{% block title %}{% trans "Zresetuj swoje hasło" %}{% endblock %}
+
+{% block content %}
+    <div class="row m-0">
+        <div class="col-lg-4 col-md-6 m-auto">
+            <div class="bg-white mt-4 p-3 clearfix">
+                <h5 class="mt-2 mb-4">{% trans "Zresetuj swoje hasło" %}</h5>
+                <form action="." method="post">
+                    {% crispy form helper %}
+                </form>
+            </div>
+        </div>
+    </div>
+{% endblock %}
diff --git a/users/templates/user_form.html b/users/templates/user_form.html
new file mode 100644
index 0000000000000000000000000000000000000000..82fbbf3540492162a340f08a1165c63a2e1fa62b
--- /dev/null
+++ b/users/templates/user_form.html
@@ -0,0 +1,13 @@
+{% extends "base-margins.html" %}
+
+{% load i18n %}
+
+{% load crispy_forms_filters %}
+
+{% block title %}{{ title }}{% endblock %}
+
+{% block content2 %}
+    <h5 class="mt-4 mb-4">{{ title }}</h5>
+
+    {% crispy form %}
+{% endblock %}
diff --git a/users/templates/user_list.html b/users/templates/user_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..c3b00516c9eac5e6ebe75f76222384c388421f16
--- /dev/null
+++ b/users/templates/user_list.html
@@ -0,0 +1,38 @@
+{% extends "base-margins.html" %}
+
+{% load i18n %}
+
+{% block title %}{% trans 'Użytkownicy' %}{% endblock %}
+
+{% block content2 %}
+<div class="mt-3">
+    <h5 class="float-left mt-2">{% trans 'Użytkownicy' %}</h5>
+    <div class="mb-4 float-right">
+        {% if perms.users.add_user %}
+            <a href="{% url 'users:user_add' %}" class="btn btn-sm btn-outline-dark">+ {% trans 'Dodaj użytkownika' %}</a>
+        {% endif %}
+    </div>
+</div>
+<table class="table">
+    <thead>
+        <tr>
+            <th>{% trans "ImiÄ™ i nazwisko" %}</th>
+            <th>{% trans "Nazwa użytkownika" %}</th>
+            <th>{% trans "Grupa" %}</th>
+            <th>{% trans "Aktywny" %}</th>
+            <th>{% trans "Akcje" %}</th>
+        </tr>
+    </thead>
+    <tbody>
+    {% for user in users %}
+        <tr>
+            <td>{{ user.get_full_name }}</td>
+            <td>{{ user.username }}</td>
+            <td>{{ user.groups.all|join:", " }}</td>
+            <td>{{ user.is_active|yesno }}</td>
+            <td><a href="{% url 'users:user_edit' pk=user.pk %}" class="btn btn-xs btn-outline-dark">{% trans 'Edytuj' %}</a></td>
+        </tr>
+    {% endfor %}
+    </tbody>
+</table>
+{% endblock %}
diff --git a/users/templates/user_profile.html b/users/templates/user_profile.html
new file mode 100644
index 0000000000000000000000000000000000000000..35db9c221e1fd076b4dce4c02ff56ab1cd350170
--- /dev/null
+++ b/users/templates/user_profile.html
@@ -0,0 +1,13 @@
+{% extends "base-margins.html" %}
+
+{% load i18n %}
+
+{% load crispy_forms_filters %}
+
+{% block title %}{% trans 'Twój profil' %}{% endblock %}
+
+{% block content2 %}
+    <h5 class="mt-4 mb-4">{% trans 'Twój profil' %}</h5>
+
+    {% crispy form %}
+{% endblock %}
diff --git a/users/urls.py b/users/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..76ae1faa248c31572c181cbb1e88b1829f8eef33
--- /dev/null
+++ b/users/urls.py
@@ -0,0 +1,29 @@
+from django.urls import include, path, reverse_lazy
+
+from django.contrib.auth import views as auth_views
+
+from . import views
+from .forms import login_form_helper, password_reset_form_helper
+
+app_name = 'users'
+
+urlpatterns = [
+    path('', views.user_list, name="user_list"),
+    path('add/', views.user_add, name="user_add"),
+    path('<int:pk>/edit/', views.user_edit, name="user_edit"),
+    path('profile/', views.user_profile, name="user_profile"),
+    path(
+        'login/',
+        auth_views.LoginView.as_view(extra_context={"helper": login_form_helper}, success_url=reverse_lazy('dash')),
+        name="login",
+    ),
+    path('logout/', auth_views.LogoutView.as_view(), name="logout"),
+    path(
+        'password-reset/',
+        auth_views.PasswordResetView.as_view(
+            template_name="registration/password_reset.html",
+            extra_context={"helper": password_reset_form_helper},
+        ),
+        name="password_reset",
+    ),
+]
diff --git a/users/views.py b/users/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..97b3b5aac9bed68aad34067282e246964e79c094
--- /dev/null
+++ b/users/views.py
@@ -0,0 +1,48 @@
+from django.contrib.auth.decorators import login_required, permission_required
+from django.contrib.auth.models import User
+from django.shortcuts import get_object_or_404, render, redirect
+from django.utils.translation import gettext_lazy as _
+
+from users.forms import UserForm, UserProfileForm
+
+
+@permission_required('users.view_user')
+def user_list(request):
+    return render(request, 'user_list.html', {'users': User.objects.order_by('username')})
+
+
+@login_required
+def user_profile(request):
+    if request.method == 'POST':
+        form = UserProfileForm(instance=request.user, data=request.POST)
+        if form.is_valid():
+            form.save()
+            return redirect('dash')
+    else:
+        form = UserProfileForm(instance=request.user)
+    return render(request, 'user_profile.html', {'form': form})
+
+
+@permission_required('users.add_user')
+def user_add(request):
+    if request.method == 'POST':
+        form = UserForm(instance=User(), data=request.POST)
+        if form.is_valid():
+            form.save()
+            return redirect('users:user_list')
+    else:
+        form = UserForm(instance=User())
+    return render(request, 'user_form.html', {'form': form, 'title': _('Dodaj użytkownika')})
+
+
+@permission_required('users.change_user')
+def user_edit(request, pk):
+    user = get_object_or_404(User, pk=pk)
+    if request.method == 'POST':
+        form = UserForm(instance=user, data=request.POST)
+        if form.is_valid():
+            form.save()
+            return redirect('users:user_list')
+    else:
+        form = UserForm(instance=user)
+    return render(request, 'user_form.html', {'form': form, 'title': _('Edytuj użytkownika')})