diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5dfbf0642691e4033bac3f82bdc6fcdeff49f878..206bb50cc4a08fd53c59dcfa64c7f44427f2c823 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -20,7 +20,7 @@ black:
   before_script:
     - pip install black
   script:
-    - black --check --diff api/
+    - black --exclude "/(\.git|\.hg|\.mypy_cache|\.tox|\.venv|_build|buck-out|build|dist|migrations)/" --check --diff api/
 
 flake8:
   image: python:3.6
diff --git a/api/config/settings/common.py b/api/config/settings/common.py
index cb5573ed58ddf3edc8ae4df14b7ecf799b0946e4..a836dfdfddb096384bfff92855a34cf478f0aaa6 100644
--- a/api/config/settings/common.py
+++ b/api/config/settings/common.py
@@ -146,6 +146,7 @@ MIDDLEWARE = (
     "django.contrib.auth.middleware.AuthenticationMiddleware",
     "django.contrib.messages.middleware.MessageMiddleware",
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
+    "funkwhale_api.users.middleware.RecordActivityMiddleware",
 )
 
 # MIGRATIONS CONFIGURATION
diff --git a/api/funkwhale_api/manage/filters.py b/api/funkwhale_api/manage/filters.py
index 2f2bde838fa90695ebc1f077e88650f2d69536c5..e4cda18c5b9de693a83adf12c4335d45249c0052 100644
--- a/api/funkwhale_api/manage/filters.py
+++ b/api/funkwhale_api/manage/filters.py
@@ -3,6 +3,7 @@ from django_filters import rest_framework as filters
 
 from funkwhale_api.common import fields
 from funkwhale_api.music import models as music_models
+from funkwhale_api.users import models as users_models
 
 
 class ManageTrackFileFilterSet(filters.FilterSet):
@@ -18,3 +19,21 @@ class ManageTrackFileFilterSet(filters.FilterSet):
     class Meta:
         model = music_models.TrackFile
         fields = ["q", "track__album", "track__artist", "track", "library_track"]
+
+
+class ManageUserFilterSet(filters.FilterSet):
+    q = fields.SearchFilter(search_fields=["username", "email", "name"])
+
+    class Meta:
+        model = users_models.User
+        fields = [
+            "q",
+            "is_active",
+            "privacy_level",
+            "is_staff",
+            "is_superuser",
+            "permission_upload",
+            "permission_library",
+            "permission_settings",
+            "permission_federation",
+        ]
diff --git a/api/funkwhale_api/manage/serializers.py b/api/funkwhale_api/manage/serializers.py
index 1c94cf5538171973a16e29a8f54591daf7778f2e..6e57db81f91a431baab736f24dda6b1a4c2e9f2e 100644
--- a/api/funkwhale_api/manage/serializers.py
+++ b/api/funkwhale_api/manage/serializers.py
@@ -3,6 +3,7 @@ from rest_framework import serializers
 
 from funkwhale_api.common import serializers as common_serializers
 from funkwhale_api.music import models as music_models
+from funkwhale_api.users import models as users_models
 
 from . import filters
 
@@ -67,3 +68,50 @@ class ManageTrackFileActionSerializer(common_serializers.ActionSerializer):
     @transaction.atomic
     def handle_delete(self, objects):
         return objects.delete()
+
+
+class PermissionsSerializer(serializers.Serializer):
+    def to_representation(self, o):
+        return o.get_permissions(defaults=self.context.get("default_permissions"))
+
+    def to_internal_value(self, o):
+        return {"permissions": o}
+
+
+class ManageUserSerializer(serializers.ModelSerializer):
+    permissions = PermissionsSerializer(source="*")
+
+    class Meta:
+        model = users_models.User
+        fields = (
+            "id",
+            "username",
+            "email",
+            "name",
+            "is_active",
+            "is_staff",
+            "is_superuser",
+            "date_joined",
+            "last_activity",
+            "permissions",
+            "privacy_level",
+        )
+        read_only_fields = [
+            "id",
+            "email",
+            "privacy_level",
+            "username",
+            "date_joined",
+            "last_activity",
+        ]
+
+    def update(self, instance, validated_data):
+        instance = super().update(instance, validated_data)
+        permissions = validated_data.pop("permissions", {})
+        if permissions:
+            for p, value in permissions.items():
+                setattr(instance, "permission_{}".format(p), value)
+            instance.save(
+                update_fields=["permission_{}".format(p) for p in permissions.keys()]
+            )
+        return instance
diff --git a/api/funkwhale_api/manage/urls.py b/api/funkwhale_api/manage/urls.py
index 60853034f0a0552c01b67b6a0354158691d49783..f208fb857079db390c6d4d4bb63e14107b2d6c11 100644
--- a/api/funkwhale_api/manage/urls.py
+++ b/api/funkwhale_api/manage/urls.py
@@ -5,7 +5,10 @@ from . import views
 
 library_router = routers.SimpleRouter()
 library_router.register(r"track-files", views.ManageTrackFileViewSet, "track-files")
+users_router = routers.SimpleRouter()
+users_router.register(r"users", views.ManageUserViewSet, "users")
 
 urlpatterns = [
-    url(r"^library/", include((library_router.urls, "instance"), namespace="library"))
+    url(r"^library/", include((library_router.urls, "instance"), namespace="library")),
+    url(r"^users/", include((users_router.urls, "instance"), namespace="users")),
 ]
diff --git a/api/funkwhale_api/manage/views.py b/api/funkwhale_api/manage/views.py
index 8511732c96b287e7c2c82da799dc864c7a455e6a..f9b78ef87a076a95afa43dc8c57fe9d6ededffc1 100644
--- a/api/funkwhale_api/manage/views.py
+++ b/api/funkwhale_api/manage/views.py
@@ -1,7 +1,9 @@
 from rest_framework import mixins, response, viewsets
 from rest_framework.decorators import list_route
 
+from funkwhale_api.common import preferences
 from funkwhale_api.music import models as music_models
+from funkwhale_api.users import models as users_models
 from funkwhale_api.users.permissions import HasUserPermission
 
 from . import filters, serializers
@@ -41,3 +43,22 @@ class ManageTrackFileViewSet(
         serializer.is_valid(raise_exception=True)
         result = serializer.save()
         return response.Response(result, status=200)
+
+
+class ManageUserViewSet(
+    mixins.ListModelMixin,
+    mixins.RetrieveModelMixin,
+    mixins.UpdateModelMixin,
+    viewsets.GenericViewSet,
+):
+    queryset = users_models.User.objects.all().order_by("-id")
+    serializer_class = serializers.ManageUserSerializer
+    filter_class = filters.ManageUserFilterSet
+    permission_classes = (HasUserPermission,)
+    required_permissions = ["settings"]
+    ordering_fields = ["date_joined", "last_activity", "username"]
+
+    def get_serializer_context(self):
+        context = super().get_serializer_context()
+        context["default_permissions"] = preferences.get("users__default_permissions")
+        return context
diff --git a/api/funkwhale_api/users/middleware.py b/api/funkwhale_api/users/middleware.py
new file mode 100644
index 0000000000000000000000000000000000000000..d5e83f0809336c335db7f7c21beaa5ac2b6995f1
--- /dev/null
+++ b/api/funkwhale_api/users/middleware.py
@@ -0,0 +1,9 @@
+class RecordActivityMiddleware:
+    def __init__(self, get_response):
+        self.get_response = get_response
+
+    def __call__(self, request):
+        response = self.get_response(request)
+        if hasattr(request, "user") and request.user.is_authenticated:
+            request.user.record_activity()
+        return response
diff --git a/api/funkwhale_api/users/migrations/0008_auto_20180617_1531.py b/api/funkwhale_api/users/migrations/0008_auto_20180617_1531.py
new file mode 100644
index 0000000000000000000000000000000000000000..b731e327951573b3092d098ab9c9b3c0dfcdf9df
--- /dev/null
+++ b/api/funkwhale_api/users/migrations/0008_auto_20180617_1531.py
@@ -0,0 +1,23 @@
+# Generated by Django 2.0.6 on 2018-06-17 15:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('users', '0007_auto_20180524_2009'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='last_activity',
+            field=models.DateTimeField(blank=True, default=None, null=True),
+        ),
+        migrations.AlterField(
+            model_name='user',
+            name='permission_library',
+            field=models.BooleanField(default=False, help_text='Manage library, delete files, tracks, artists, albums...', verbose_name='Manage library'),
+        ),
+    ]
diff --git a/api/funkwhale_api/users/models.py b/api/funkwhale_api/users/models.py
index caf1e452bbcab42ff52587a50d35c6039fdf132c..15d16db2369967cddf38310d871c6bae3733dab0 100644
--- a/api/funkwhale_api/users/models.py
+++ b/api/funkwhale_api/users/models.py
@@ -2,6 +2,7 @@
 from __future__ import absolute_import, unicode_literals
 
 import binascii
+import datetime
 import os
 import uuid
 
@@ -9,6 +10,7 @@ from django.conf import settings
 from django.contrib.auth.models import AbstractUser
 from django.db import models
 from django.urls import reverse
+from django.utils import timezone
 from django.utils.encoding import python_2_unicode_compatible
 from django.utils.translation import ugettext_lazy as _
 
@@ -75,11 +77,13 @@ class User(AbstractUser):
         default=False,
     )
 
+    last_activity = models.DateTimeField(default=None, null=True, blank=True)
+
     def __str__(self):
         return self.username
 
-    def get_permissions(self):
-        defaults = preferences.get("users__default_permissions")
+    def get_permissions(self, defaults=None):
+        defaults = defaults or preferences.get("users__default_permissions")
         perms = {}
         for p in PERMISSIONS:
             v = (
@@ -90,6 +94,10 @@ class User(AbstractUser):
             perms[p] = v
         return perms
 
+    @property
+    def all_permissions(self):
+        return self.get_permissions()
+
     def has_permissions(self, *perms, **kwargs):
         operator = kwargs.pop("operator", "and")
         if operator not in ["and", "or"]:
@@ -117,3 +125,16 @@ class User(AbstractUser):
 
     def get_activity_url(self):
         return settings.FUNKWHALE_URL + "/@{}".format(self.username)
+
+    def record_activity(self):
+        """
+        Simply update the last_activity field if current value is too old
+        than a threshold. This is useful to keep a track of inactive accounts.
+        """
+        current = self.last_activity
+        delay = 60 * 15  # fifteen minutes
+        now = timezone.now()
+
+        if current is None or current < now - datetime.timedelta(seconds=delay):
+            self.last_activity = now
+            self.save(update_fields=["last_activity"])
diff --git a/api/tests/conftest.py b/api/tests/conftest.py
index 40203ee3d472693d7304d92ab68094949535fab9..aa36e1f76f60d836a33b0c82a44be55a4f3cb372 100644
--- a/api/tests/conftest.py
+++ b/api/tests/conftest.py
@@ -7,6 +7,7 @@ import pytest
 import requests_mock
 from django.contrib.auth.models import AnonymousUser
 from django.core.cache import cache as django_cache
+from django.utils import timezone
 from django.test import client
 from dynamic_preferences.registries import global_preferences_registry
 from rest_framework import fields as rest_fields
@@ -250,3 +251,10 @@ def to_api_date():
         raise ValueError("Invalid value: {}".format(value))
 
     return inner
+
+
+@pytest.fixture()
+def now(mocker):
+    now = timezone.now()
+    mocker.patch("django.utils.timezone.now", return_value=now)
+    return now
diff --git a/api/tests/manage/test_serializers.py b/api/tests/manage/test_serializers.py
index 893cfd86e46e5720446c78dcde5457a7358206b1..2f0c6bc2568e4a2a7c9e9755a00eda50ecd9dd92 100644
--- a/api/tests/manage/test_serializers.py
+++ b/api/tests/manage/test_serializers.py
@@ -8,3 +8,26 @@ def test_manage_track_file_action_delete(factories):
     s.handle_delete(tfs.__class__.objects.all())
 
     assert tfs.__class__.objects.count() == 0
+
+
+def test_user_update_permission(factories):
+    user = factories["users.User"](
+        permission_library=False,
+        permission_upload=False,
+        permission_federation=True,
+        permission_settings=True,
+        is_active=True,
+    )
+    s = serializers.ManageUserSerializer(
+        user,
+        data={"is_active": False, "permissions": {"federation": False, "upload": True}},
+    )
+    s.is_valid(raise_exception=True)
+    s.save()
+    user.refresh_from_db()
+
+    assert user.is_active is False
+    assert user.permission_federation is False
+    assert user.permission_upload is True
+    assert user.permission_library is False
+    assert user.permission_settings is True
diff --git a/api/tests/manage/test_views.py b/api/tests/manage/test_views.py
index e2bfbf3a81511dbf313cd630e1d1353840e38b83..a72bcf5af4fa77b92a25ab5dd099684bb196f13c 100644
--- a/api/tests/manage/test_views.py
+++ b/api/tests/manage/test_views.py
@@ -5,7 +5,11 @@ from funkwhale_api.manage import serializers, views
 
 
 @pytest.mark.parametrize(
-    "view,permissions,operator", [(views.ManageTrackFileViewSet, ["library"], "and")]
+    "view,permissions,operator",
+    [
+        (views.ManageTrackFileViewSet, ["library"], "and"),
+        (views.ManageUserViewSet, ["settings"], "and"),
+    ],
 )
 def test_permissions(assert_user_permission, view, permissions, operator):
     assert_user_permission(view, permissions, operator)
@@ -23,3 +27,18 @@ def test_track_file_view(factories, superuser_api_client):
 
     assert response.data["count"] == len(tfs)
     assert response.data["results"] == expected
+
+
+def test_user_view(factories, superuser_api_client, mocker):
+    mocker.patch("funkwhale_api.users.models.User.record_activity")
+    users = factories["users.User"].create_batch(size=5) + [superuser_api_client.user]
+    qs = users[0].__class__.objects.order_by("-id")
+    url = reverse("api:v1:manage:users:users-list")
+
+    response = superuser_api_client.get(url, {"sort": "-id"})
+    expected = serializers.ManageUserSerializer(
+        qs, many=True, context={"request": response.wsgi_request}
+    ).data
+
+    assert response.data["count"] == len(users)
+    assert response.data["results"] == expected
diff --git a/api/tests/users/test_middleware.py b/api/tests/users/test_middleware.py
new file mode 100644
index 0000000000000000000000000000000000000000..fd13df4b31b7e8cefb1fa61347574b43fc6ab727
--- /dev/null
+++ b/api/tests/users/test_middleware.py
@@ -0,0 +1,18 @@
+from funkwhale_api.users import middleware
+
+
+def test_record_activity_middleware(factories, api_request, mocker):
+    m = middleware.RecordActivityMiddleware(lambda request: None)
+    user = factories["users.User"]()
+    record_activity = mocker.patch("funkwhale_api.users.models.User.record_activity")
+    request = api_request.get("/")
+    request.user = user
+    m(request)
+
+    record_activity.assert_called_once_with()
+
+
+def test_record_activity_middleware_no_user(api_request):
+    m = middleware.RecordActivityMiddleware(lambda request: None)
+    request = api_request.get("/")
+    m(request)
diff --git a/api/tests/users/test_models.py b/api/tests/users/test_models.py
index c73a4a1b1a4b4cd351278d7757fb2b0307e3b193..74bb091e54270c4034050b7810e2b6ba3cb4bde3 100644
--- a/api/tests/users/test_models.py
+++ b/api/tests/users/test_models.py
@@ -78,3 +78,20 @@ def test_has_permissions_and(args, perms, expected, factories):
 def test_has_permissions_or(args, perms, expected, factories):
     user = factories["users.User"](**args)
     assert user.has_permissions(*perms, operator="or") is expected
+
+
+def test_record_activity(factories, now):
+    user = factories["users.User"]()
+    assert user.last_activity is None
+
+    user.record_activity()
+
+    assert user.last_activity == now
+
+
+def test_record_activity_does_nothing_if_already(factories, now, mocker):
+    user = factories["users.User"](last_activity=now)
+    save = mocker.patch("funkwhale_api.users.models.User.save")
+    user.record_activity()
+
+    save.assert_not_called()
diff --git a/changes/changelog.d/212.feature b/changes/changelog.d/212.feature
new file mode 100644
index 0000000000000000000000000000000000000000..0d029856b7c808bec9f9b2da8c71e5bb1b211ceb
--- /dev/null
+++ b/changes/changelog.d/212.feature
@@ -0,0 +1 @@
+Management interface for users (#212)
diff --git a/front/src/components/Sidebar.vue b/front/src/components/Sidebar.vue
index d46fb846cf1d76fd23265b18f1fd34a6e361dfe7..03ea4ee0773a022acd0ef5d9cbd36c554330f6af 100644
--- a/front/src/components/Sidebar.vue
+++ b/front/src/components/Sidebar.vue
@@ -96,6 +96,12 @@
               :to="{path: '/manage/settings'}">
               <i class="settings icon"></i>{{ $t('Settings') }}
             </router-link>
+            <router-link
+              class="item"
+              v-if="$store.state.auth.availablePermissions['settings']"
+              :to="{path: '/manage/users'}">
+              <i class="users icon"></i>{{ $t('Users') }}
+            </router-link>
           </div>
         </div>
       </div>
diff --git a/front/src/components/common/ActionTable.vue b/front/src/components/common/ActionTable.vue
index 5221c328292c317c199850d6c9a3de58fec188f9..f23479066e37667d3ea7dba63d504f1778a93d07 100644
--- a/front/src/components/common/ActionTable.vue
+++ b/front/src/components/common/ActionTable.vue
@@ -61,7 +61,7 @@
         </th>
       </tr>
       <tr>
-        <th>
+        <th v-if="actions.length > 0">
           <div class="ui checkbox">
             <input
               type="checkbox"
@@ -75,7 +75,7 @@
     </thead>
     <tbody v-if="objectsData.count > 0">
       <tr v-for="(obj, index) in objectsData.results">
-        <td class="collapsing">
+        <td v-if="actions.length > 0" class="collapsing">
           <input
             type="checkbox"
             :disabled="checkable.indexOf(obj.id) === -1"
@@ -184,6 +184,9 @@ export default {
       })[0]
     },
     checkable () {
+      if (!this.currentAction) {
+        return []
+      }
       let objs = this.objectsData.results
       let filter = this.currentAction.filterCheckable
       if (filter) {
diff --git a/front/src/components/manage/users/UsersTable.vue b/front/src/components/manage/users/UsersTable.vue
new file mode 100644
index 0000000000000000000000000000000000000000..5658583c3df1d59b51d186123beb6fa15aa6dd29
--- /dev/null
+++ b/front/src/components/manage/users/UsersTable.vue
@@ -0,0 +1,216 @@
+<template>
+  <div>
+    <div class="ui inline form">
+      <div class="fields">
+        <div class="ui field">
+          <label>{{ $t('Search') }}</label>
+          <input type="text" v-model="search" placeholder="Search by username, email, name..." />
+        </div>
+        <div class="field">
+          <i18next tag="label" path="Ordering"/>
+          <select class="ui dropdown" v-model="ordering">
+            <option v-for="option in orderingOptions" :value="option[0]">
+              {{ option[1] }}
+            </option>
+          </select>
+        </div>
+        <div class="field">
+          <i18next tag="label" path="Ordering direction"/>
+          <select class="ui dropdown" v-model="orderingDirection">
+            <option value="+">{{ $t('Ascending') }}</option>
+            <option value="-">{{ $t('Descending') }}</option>
+          </select>
+        </div>
+      </div>
+      </div>
+    <div class="dimmable">
+      <div v-if="isLoading" class="ui active inverted dimmer">
+          <div class="ui loader"></div>
+      </div>
+      <action-table
+        v-if="result"
+        @action-launched="fetchData"
+        :objects-data="result"
+        :actions="actions"
+        :action-url="'manage/library/track-files/action/'"
+        :filters="actionFilters">
+        <template slot="header-cells">
+          <th>{{ $t('Username') }}</th>
+          <th>{{ $t('Email') }}</th>
+          <th>{{ $t('Account status') }}</th>
+          <th>{{ $t('Sign-up') }}</th>
+          <th>{{ $t('Last activity') }}</th>
+          <th>{{ $t('Permissions') }}</th>
+          <th>{{ $t('Status') }}</th>
+        </template>
+        <template slot="row-cells" slot-scope="scope">
+          <td>
+            <router-link :to="{name: 'manage.users.detail', params: {id: scope.obj.id }}">{{ scope.obj.username }}</router-link>
+          </td>
+          <td>
+            <span>{{ scope.obj.email }}</span>
+          </td>
+          <td>
+            <span v-if="scope.obj.is_active" class="ui basic green label">{{ $t('Active') }}</span>
+            <span v-else class="ui basic grey label">{{ $t('Inactive') }}</span>
+          </td>
+          <td>
+            <human-date :date="scope.obj.date_joined"></human-date>
+          </td>
+          <td>
+            <human-date v-if="scope.obj.last_activity" :date="scope.obj.last_activity"></human-date>
+            <template v-else>{{ $t('N/A') }}</template>
+          </td>
+          <td>
+            <template v-for="p in permissions">
+              <span class="ui basic tiny label" v-if="scope.obj.permissions[p.code]">{{ p.label }}</span>
+            </template>
+          </td>
+          <td>
+            <span v-if="scope.obj.is_superuser" class="ui pink label">{{ $t('Admin') }}</span>
+            <span v-else-if="scope.obj.is_staff" class="ui purple label">{{ $t('Staff member') }}</span>
+            <span v-else class="ui basic label">{{ $t('regular user') }}</span>
+          </td>
+        </template>
+      </action-table>
+    </div>
+    <div>
+      <pagination
+        v-if="result && result.results.length > 0"
+        @page-changed="selectPage"
+        :compact="true"
+        :current="page"
+        :paginate-by="paginateBy"
+        :total="result.count"
+        ></pagination>
+
+      <span v-if="result && result.results.length > 0">
+        {{ $t('Showing results {%start%}-{%end%} on {%total%}', {start: ((page-1) * paginateBy) + 1 , end: ((page-1) * paginateBy) + result.results.length, total: result.count})}}
+      </span>
+    </div>
+  </div>
+</template>
+
+<script>
+import axios from 'axios'
+import _ from 'lodash'
+import time from '@/utils/time'
+import Pagination from '@/components/Pagination'
+import ActionTable from '@/components/common/ActionTable'
+import OrderingMixin from '@/components/mixins/Ordering'
+
+export default {
+  mixins: [OrderingMixin],
+  props: {
+    filters: {type: Object, required: false}
+  },
+  components: {
+    Pagination,
+    ActionTable
+  },
+  data () {
+    let defaultOrdering = this.getOrderingFromString(this.defaultOrdering || '-date_joined')
+    return {
+      time,
+      isLoading: false,
+      result: null,
+      page: 1,
+      paginateBy: 50,
+      search: '',
+      orderingDirection: defaultOrdering.direction || '+',
+      ordering: defaultOrdering.field,
+      orderingOptions: [
+        ['date_joined', 'Sign-up date'],
+        ['last_activity', 'Last activity'],
+        ['username', 'Username']
+      ]
+
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      let params = _.merge({
+        'page': this.page,
+        'page_size': this.paginateBy,
+        'q': this.search,
+        'ordering': this.getOrderingAsString()
+      }, this.filters)
+      let self = this
+      self.isLoading = true
+      self.checked = []
+      axios.get('/manage/users/users/', {params: params}).then((response) => {
+        self.result = response.data
+        self.isLoading = false
+      }, error => {
+        self.isLoading = false
+        self.errors = error.backendErrors
+      })
+    },
+    selectPage: function (page) {
+      this.page = page
+    }
+  },
+  computed: {
+    privacyLevels () {
+      return {}
+    },
+    permissions () {
+      return [
+        {
+          'code': 'upload',
+          'label': this.$t('Upload')
+        },
+        {
+          'code': 'library',
+          'label': this.$t('Library')
+        },
+        {
+          'code': 'federation',
+          'label': this.$t('Federation')
+        },
+        {
+          'code': 'settings',
+          'label': this.$t('Settings')
+        }
+      ]
+    },
+    actionFilters () {
+      var currentFilters = {
+        q: this.search
+      }
+      if (this.filters) {
+        return _.merge(currentFilters, this.filters)
+      } else {
+        return currentFilters
+      }
+    },
+    actions () {
+      return [
+        // {
+        //   name: 'delete',
+        //   label: this.$t('Delete'),
+        //   isDangerous: true
+        // }
+      ]
+    }
+  },
+  watch: {
+    search (newValue) {
+      this.page = 1
+      this.fetchData()
+    },
+    page () {
+      this.fetchData()
+    },
+    ordering () {
+      this.fetchData()
+    },
+    orderingDirection () {
+      this.fetchData()
+    }
+  }
+}
+</script>
diff --git a/front/src/router/index.js b/front/src/router/index.js
index a52070e35912b42813db85f3c8ac195f6e39e4d2..0d2ad34f98aceff40b212625bac52b9ab9be9bf7 100644
--- a/front/src/router/index.js
+++ b/front/src/router/index.js
@@ -31,6 +31,9 @@ import Favorites from '@/components/favorites/List'
 import AdminSettings from '@/views/admin/Settings'
 import AdminLibraryBase from '@/views/admin/library/Base'
 import AdminLibraryFilesList from '@/views/admin/library/FilesList'
+import AdminUsersBase from '@/views/admin/users/Base'
+import AdminUsersDetail from '@/views/admin/users/UsersDetail'
+import AdminUsersList from '@/views/admin/users/UsersList'
 import FederationBase from '@/views/federation/Base'
 import FederationScan from '@/views/federation/Scan'
 import FederationLibraryDetail from '@/views/federation/LibraryDetail'
@@ -180,6 +183,23 @@ export default new Router({
         }
       ]
     },
+    {
+      path: '/manage/users',
+      component: AdminUsersBase,
+      children: [
+        {
+          path: '',
+          name: 'manage.users.list',
+          component: AdminUsersList
+        },
+        {
+          path: ':id',
+          name: 'manage.users.detail',
+          component: AdminUsersDetail,
+          props: true
+        }
+      ]
+    },
     {
       path: '/library',
       component: Library,
diff --git a/front/src/views/admin/users/Base.vue b/front/src/views/admin/users/Base.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e545b7f709fd63cad5154c4c0375b067f689e72f
--- /dev/null
+++ b/front/src/views/admin/users/Base.vue
@@ -0,0 +1,28 @@
+<template>
+  <div class="main pusher"  v-title="$t('Manage users')">
+    <div class="ui secondary pointing menu">
+      <router-link
+        class="ui item"
+        :to="{name: 'manage.users.list'}">{{ $t('Users') }}</router-link>
+    </div>
+    <router-view :key="$route.fullPath"></router-view>
+  </div>
+</template>
+
+<script>
+export default {}
+</script>
+
+<style lang="scss">
+@import '../../../style/vendor/media';
+
+.main.pusher > .ui.secondary.menu {
+  @include media(">tablet") {
+    margin: 0 2.5rem;
+  }
+  .item {
+    padding-top: 1.5em;
+    padding-bottom: 1.5em;
+  }
+}
+</style>
diff --git a/front/src/views/admin/users/UsersDetail.vue b/front/src/views/admin/users/UsersDetail.vue
new file mode 100644
index 0000000000000000000000000000000000000000..ea92716ca943570d77e1cd4cd91cb1d9eaa97133
--- /dev/null
+++ b/front/src/views/admin/users/UsersDetail.vue
@@ -0,0 +1,177 @@
+<template>
+  <div>
+    <div v-if="isLoading" class="ui vertical segment">
+      <div :class="['ui', 'centered', 'active', 'inline', 'loader']"></div>
+    </div>
+    <template v-if="object">
+      <div :class="['ui', 'head', 'vertical', 'center', 'aligned', 'stripe', 'segment']" v-title="object.username">
+        <div class="segment-content">
+          <h2 class="ui center aligned icon header">
+            <i class="circular inverted user red icon"></i>
+            <div class="content">
+              @{{ object.username }}
+            </div>
+          </h2>
+        </div>
+        <div class="ui hidden divider"></div>
+        <div class="ui one column centered grid">
+          <table class="ui collapsing very basic table">
+            <tbody>
+              <tr>
+                <td>
+                  {{ $t('Name') }}
+                </td>
+                <td>
+                  {{ object.name }}
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Email address') }}
+                </td>
+                <td>
+                  {{ object.email }}
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Sign-up') }}
+                </td>
+                <td>
+                  <human-date :date="object.date_joined"></human-date>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Last activity') }}
+                </td>
+                <td>
+                  <human-date v-if="object.last_activity" :date="object.last_activity"></human-date>
+                  <template v-else>{{ $t('N/A') }}</template>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Account active') }}
+                  <span :data-tooltip="$t('Determine if the user account is active or not. Inactive users cannot login or user the service.')"><i class="question circle icon"></i></span>
+                </td>
+                <td>
+                  <div class="ui toggle checkbox">
+                    <input
+                      @change="update('is_active')"
+                      v-model="object.is_active" type="checkbox">
+                    <label></label>
+                  </div>
+                </td>
+              </tr>
+              <tr>
+                <td>
+                  {{ $t('Permissions') }}
+                </td>
+                <td>
+                  <select
+                    @change="update('permissions')"
+                    v-model="permissions"
+                    multiple
+                    class="ui search selection dropdown">
+                    <option v-for="p in allPermissions" :value="p.code">{{ p.label }}</option>
+                  </select>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+        <div class="ui hidden divider"></div>
+        <button @click="fetchData" class="ui basic button">{{ $t('Refresh') }}</button>
+      </div>
+    </template>
+  </div>
+</template>
+
+<script>
+
+import $ from 'jquery'
+import axios from 'axios'
+import logger from '@/logging'
+
+export default {
+  props: ['id'],
+  data () {
+    return {
+      isLoading: true,
+      object: null,
+      permissions: []
+    }
+  },
+  created () {
+    this.fetchData()
+  },
+  methods: {
+    fetchData () {
+      var self = this
+      this.isLoading = true
+      let url = 'manage/users/users/' + this.id + '/'
+      axios.get(url).then((response) => {
+        self.object = response.data
+        self.permissions = []
+        self.allPermissions.forEach(p => {
+          if (self.object.permissions[p.code]) {
+            self.permissions.push(p.code)
+          }
+        })
+        self.isLoading = false
+      })
+    },
+    update (attr) {
+      let newValue = this.object[attr]
+      let params = {}
+      if (attr === 'permissions') {
+        params['permissions'] = {}
+        this.allPermissions.forEach(p => {
+          params['permissions'][p.code] = this.permissions.indexOf(p.code) > -1
+        })
+      } else {
+        params[attr] = newValue
+      }
+      axios.patch('manage/users/users/' + this.id + '/', params).then((response) => {
+        logger.default.info(`${attr} was updated succcessfully to ${newValue}`)
+      }, (error) => {
+        logger.default.error(`Error while setting ${attr} to ${newValue}`, error)
+      })
+    }
+  },
+  computed: {
+    allPermissions () {
+      return [
+        {
+          'code': 'upload',
+          'label': this.$t('Upload')
+        },
+        {
+          'code': 'library',
+          'label': this.$t('Library')
+        },
+        {
+          'code': 'federation',
+          'label': this.$t('Federation')
+        },
+        {
+          'code': 'settings',
+          'label': this.$t('Settings')
+        }
+      ]
+    }
+  },
+  watch: {
+    object () {
+      this.$nextTick(() => {
+        $('select.dropdown').dropdown()
+      })
+    }
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>
diff --git a/front/src/views/admin/users/UsersList.vue b/front/src/views/admin/users/UsersList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b22d4aaf836f0b45be74d2a894c59963eac8a145
--- /dev/null
+++ b/front/src/views/admin/users/UsersList.vue
@@ -0,0 +1,23 @@
+<template>
+  <div v-title="$t('Users')">
+    <div class="ui vertical stripe segment">
+      <h2 class="ui header">{{ $t('Users') }}</h2>
+      <div class="ui hidden divider"></div>
+      <users-table></users-table>
+    </div>
+  </div>
+</template>
+
+<script>
+import UsersTable from '@/components/manage/users/UsersTable'
+
+export default {
+  components: {
+    UsersTable
+  }
+}
+</script>
+
+<!-- Add "scoped" attribute to limit CSS to this component only -->
+<style scoped>
+</style>