diff --git a/app/api/users.py b/app/api/user.py
similarity index 63%
rename from app/api/users.py
rename to app/api/user.py
index 992b2c22e391a62c23ceff003906243d963ab20b..8868d3043e36b0e6cceda985e6455f17710d9b3b 100644
--- a/app/api/users.py
+++ b/app/api/user.py
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """
-app.api.users
-~~~~~~~~~~~~~
+app.api.user
+~~~~~~~~~~~~
 
-This module implements the users API.
+This module implements the user API.
 
 :copyright: (c) 2017 European Spallation Source ERIC
 :license: BSD 2-Clause, see LICENSE for more details.
@@ -11,10 +11,29 @@ This module implements the users API.
 """
 from flask import current_app, Blueprint, jsonify, request
 from flask_ldap3_login import AuthenticationResponseStatus
+from flask_jwt_extended import jwt_required
 from ..extensions import ldap_manager
-from .. import utils, tokens
+from ..decorators import jwt_groups_accepted
+from .. import utils, tokens, models
+from .utils import get_generic_model, create_generic_model
 
-bp = Blueprint('users_api', __name__)
+bp = Blueprint('user_api', __name__)
+
+
+@bp.route('/users')
+@jwt_required
+def get_users():
+    return get_generic_model(models.User, request.args,
+                             order_by=models.User.username)
+
+
+@bp.route('/users', methods=['POST'])
+@jwt_required
+@jwt_groups_accepted('admin')
+def create_user():
+    """Create a new user"""
+    return create_generic_model(models.User, mandatory_fields=(
+        'username', 'display_name', 'email'))
 
 
 @bp.route('/login', methods=['POST'])
diff --git a/app/factory.py b/app/factory.py
index ec504bab89fdaee571691a9e5bcce2c880d17ed0..7c393840dfae07397c0972e0b013c978a5fe1640 100644
--- a/app/factory.py
+++ b/app/factory.py
@@ -20,8 +20,8 @@ from .admin.views import (AdminModelView, ItemAdmin, UserAdmin, GroupAdmin, Toke
 from .main.views import bp as main
 from .inventory.views import bp as inventory
 from .network.views import bp as network
-from .users.views import bp as users
-from .api.users import bp as users_api
+from .user.views import bp as user
+from .api.user import bp as user_api
 from .api.inventory import bp as inventory_api
 from .api.network import bp as network_api
 from .defaults import defaults
@@ -91,7 +91,7 @@ def create_app(config=None):
     db.init_app(app)
     migrate.init_app(app)
     login_manager.init_app(app)
-    login_manager.login_view = 'users.login'
+    login_manager.login_view = 'user.login'
     ldap_manager.init_app(app)
     mail.init_app(app)
     jwt.init_app(app)
@@ -102,7 +102,7 @@ def create_app(config=None):
 
     admin.init_app(app)
     admin.add_view(GroupAdmin(models.Group, db.session))
-    admin.add_view(UserAdmin(models.User, db.session))
+    admin.add_view(UserAdmin(models.User, db.session, endpoint='users'))
     admin.add_view(TokenAdmin(models.Token, db.session))
     admin.add_view(AdminModelView(models.Action, db.session))
     admin.add_view(AdminModelView(models.Manufacturer, db.session))
@@ -122,8 +122,8 @@ def create_app(config=None):
     app.register_blueprint(main)
     app.register_blueprint(inventory, url_prefix='/inventory')
     app.register_blueprint(network, url_prefix='/network')
-    app.register_blueprint(users, url_prefix='/users')
-    app.register_blueprint(users_api, url_prefix='/api/v1/users')
+    app.register_blueprint(user, url_prefix='/user')
+    app.register_blueprint(user_api, url_prefix='/api/v1/user')
     app.register_blueprint(inventory_api, url_prefix='/api/v1/inventory')
     app.register_blueprint(network_api, url_prefix='/api/v1/network')
 
diff --git a/app/models.py b/app/models.py
index a4e6e8d80dc0d0b9e7355b1398fac98b46fdb67f..1a9c29f30a12916ca073205a8bcbc513807e614b 100644
--- a/app/models.py
+++ b/app/models.py
@@ -85,7 +85,7 @@ def save_user(dn, username, data, memberships):
     user = User.query.filter_by(username=username).first()
     if user is None:
         user = User(username=username,
-                    name=utils.attribute_to_string(data['cn']),
+                    display_name=utils.attribute_to_string(data['cn']),
                     email=utils.attribute_to_string(data['mail']))
     # Always update the user groups to keep them up-to-date
     user.groups = [utils.attribute_to_string(group['cn']) for group in memberships]
@@ -122,8 +122,8 @@ class User(db.Model, UserMixin):
     __tablename__ = 'user_account'
 
     id = db.Column(db.Integer, primary_key=True)
-    username = db.Column(db.Text, unique=True)
-    name = db.Column(db.Text)
+    username = db.Column(db.Text, nullable=False, unique=True)
+    display_name = db.Column(db.Text, nullable=False)
     email = db.Column(db.Text)
     grp = db.relationship('Group', secondary=usergroups_table,
                           backref=db.backref('members', lazy='dynamic'))
@@ -163,7 +163,16 @@ class User(db.Model, UserMixin):
         return set(names).issubset(self.groups)
 
     def __str__(self):
-        return self.name
+        return self.display_name
+
+    def to_dict(self):
+        return {
+            'id': self.id,
+            'username': self.username,
+            'display_name': self.display_name,
+            'email': self.email,
+            'groups': self.csentry_groups,
+        }
 
 
 class Token(db.Model):
diff --git a/app/templates/base.html b/app/templates/base.html
index 03806190b1373929291d93472f2d291c254207d0..499fda31924c6c5f4dc7dac677636b308d37515f 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -32,13 +32,13 @@
         </div>
         <div class="navbar-nav">
           {% if current_user.is_authenticated %}
-          <div class="dropdown {{ is_active(path == "/users/profile") }}">
+          <div class="dropdown {{ is_active(path == "/user/profile") }}">
             <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
               {{current_user}}
             </a>
             <div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
-              <a class="dropdown-item" href="{{ url_for('users.profile') }}">Profile</a>
-              <a class="dropdown-item" href="{{ url_for('users.logout') }}">Logout</a>
+              <a class="dropdown-item" href="{{ url_for('user.profile') }}">Profile</a>
+              <a class="dropdown-item" href="{{ url_for('user.logout') }}">Logout</a>
             </div>
           </div>
           {% endif %}
diff --git a/app/templates/users/login.html b/app/templates/user/login.html
similarity index 100%
rename from app/templates/users/login.html
rename to app/templates/user/login.html
diff --git a/app/templates/users/profile.html b/app/templates/user/profile.html
similarity index 96%
rename from app/templates/users/profile.html
rename to app/templates/user/profile.html
index 0eb3c463f100073b9e68e45dca2c9a0b94a2c975..f91800c812621fb8ad4a05a5d1d11fbb1398db50 100644
--- a/app/templates/users/profile.html
+++ b/app/templates/user/profile.html
@@ -22,7 +22,7 @@
   <h2>{{ user.username }}</h2>
   <dl>
     <dt>Name</dt>
-    <dd>{{user.name}}</dd>
+    <dd>{{user.display_name}}</dd>
     <dt>Email</dt>
     <dd>{{user.email}}</dd>
     <dt>CSEntry Groups</dt>
@@ -47,7 +47,7 @@
       {% for token in user.tokens %}
       <tr>
         <td>
-          <form method="POST" action="/users/tokens/revoke">
+          <form method="POST" action="/user/tokens/revoke">
             <input id="token_id" name="token_id" type="hidden" value="{{ token.id }}">
             <input id="jti" name="jti" type="hidden" value="{{ token.jti }}">
             {{ delete_button_with_confirmation("Revoke token", "revokeConfirmation-%s" | format(token.id),
diff --git a/app/users/__init__.py b/app/user/__init__.py
similarity index 100%
rename from app/users/__init__.py
rename to app/user/__init__.py
diff --git a/app/users/forms.py b/app/user/forms.py
similarity index 81%
rename from app/users/forms.py
rename to app/user/forms.py
index 06555e826c7ce17ffe85cd670c41503bfb608ff3..38e50476c173c21b7fb9f8d0e597bf7080b68813 100644
--- a/app/users/forms.py
+++ b/app/user/forms.py
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """
-app.users.forms
-~~~~~~~~~~~~~~~
+app.user.forms
+~~~~~~~~~~~~~~
 
-This module defines the users forms.
+This module defines the user blueprint forms.
 
 :copyright: (c) 2017 European Spallation Source ERIC
 :license: BSD 2-Clause, see LICENSE for more details.
diff --git a/app/users/views.py b/app/user/views.py
similarity index 85%
rename from app/users/views.py
rename to app/user/views.py
index 66cadcd9625bf70b360f06590e0211fbc3b3145c..473f9bc80ac0d8a3859bc48f2b81c9f08ab35278 100644
--- a/app/users/views.py
+++ b/app/user/views.py
@@ -1,9 +1,9 @@
 # -*- coding: utf-8 -*-
 """
-app.users.views
-~~~~~~~~~~~~~~~
+app.user.views
+~~~~~~~~~~~~~~
 
-This module implements the users blueprint.
+This module implements the user blueprint.
 
 :copyright: (c) 2017 European Spallation Source ERIC
 :license: BSD 2-Clause, see LICENSE for more details.
@@ -16,7 +16,7 @@ from flask_ldap3_login.forms import LDAPLoginForm
 from .forms import TokenForm
 from .. import tokens, utils
 
-bp = Blueprint('users', __name__)
+bp = Blueprint('user', __name__)
 
 
 @bp.route('/login', methods=['GET', 'POST'])
@@ -25,14 +25,14 @@ def login():
     if form.validate_on_submit():
         login_user(form.user, remember=form.remember_me.data)
         return redirect(request.args.get('next') or url_for('main.index'))
-    return render_template('users/login.html', form=form)
+    return render_template('user/login.html', form=form)
 
 
 @bp.route('/logout')
 @login_required
 def logout():
     logout_user()
-    return redirect(url_for('users.login'))
+    return redirect(url_for('user.login'))
 
 
 @bp.route('/profile', methods=['GET', 'POST'])
@@ -48,8 +48,8 @@ def profile():
         # Save token to the session to retrieve it after the redirect
         session['generated_token'] = token
         flash('Make sure to copy your new personal access token now. You won’t be able to see it again!', 'success')
-        return redirect(url_for('users.profile'))
-    return render_template('users/profile.html',
+        return redirect(url_for('user.profile'))
+    return render_template('user/profile.html',
                            form=form,
                            user=current_user,
                            generated_token=token)
@@ -67,4 +67,4 @@ def revoke_token():
         flash(f'Could not revoke the token {jti}. Please contact an administrator.', 'error')
     else:
         flash(f'Token {jti} has been revoked', 'success')
-    return redirect(url_for('users.profile'))
+    return redirect(url_for('user.profile'))
diff --git a/tests/functional/factories.py b/tests/functional/factories.py
index c55a2a0511faea4bba467d810c332a96d95002fe..f71df8a0aede7ddfed2318cd7f722659157b0acb 100644
--- a/tests/functional/factories.py
+++ b/tests/functional/factories.py
@@ -26,7 +26,7 @@ class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
         sqlalchemy_session_persistence = 'commit'
 
     username = factory.Sequence(lambda n: f'username{n}')
-    name = factory.LazyAttribute(lambda o: f'long {o.username}')
+    display_name = factory.LazyAttribute(lambda o: f'long {o.username}')
 
 
 class ActionFactory(factory.alchemy.SQLAlchemyModelFactory):
diff --git a/tests/functional/test_api.py b/tests/functional/test_api.py
index 5a14403108e8005dcd82cfbf6cf0be87b7758d45..781302c3b09c22f026e4a0d6384604a1149a714c 100644
--- a/tests/functional/test_api.py
+++ b/tests/functional/test_api.py
@@ -69,7 +69,7 @@ def login(client, username, password):
         'username': username,
         'password': password
     }
-    return post(client, f'{API_URL}/users/login', data)
+    return post(client, f'{API_URL}/user/login', data)
 
 
 def get_token(client, username, password):
@@ -123,9 +123,9 @@ def check_input_is_subset_of_response(response, inputs):
 
 
 def test_login(client):
-    response = client.post(f'{API_URL}/users/login')
+    response = client.post(f'{API_URL}/user/login')
     check_response_message(response, 'Body should be a JSON object')
-    response = post(client, f'{API_URL}/users/login', data={'username': 'foo', 'passwd': ''})
+    response = post(client, f'{API_URL}/user/login', data={'username': 'foo', 'passwd': ''})
     check_response_message(response, 'Missing mandatory field (username or password)', 422)
     response = login(client, 'foo', 'invalid')
     check_response_message(response, 'Invalid credentials', 401)
diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py
index fc98d6fca37dd6c7c2a45927e0395138786c8f10..1d8dac33c9ceffec0bbddebcd782ffe2d7a63f78 100644
--- a/tests/functional/test_web.py
+++ b/tests/functional/test_web.py
@@ -25,11 +25,11 @@ def login(client, username, password):
         'username': username,
         'password': password
     }
-    return client.post('/users/login', data=data, follow_redirects=True)
+    return client.post('/user/login', data=data, follow_redirects=True)
 
 
 def logout(client):
-    return client.get('/users/logout', follow_redirects=True)
+    return client.get('/user/logout', follow_redirects=True)
 
 
 @pytest.fixture
@@ -65,7 +65,7 @@ def test_index(logged_client):
 def test_protected_url(url, client):
     response = client.get(url)
     assert response.status_code == 302
-    assert '/users/login' in response.headers['Location']
+    assert '/user/login' in response.headers['Location']
     login(client, 'user_ro', 'userro')
     response = client.get(url)
     assert response.status_code == 200