Merge pull request #2017 from matmair/fr-1421-sso

SSO!
This commit is contained in:
Oliver 2021-10-11 17:20:08 +11:00 committed by GitHub
commit 19a8c712d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 712 additions and 405 deletions

View File

@ -13,7 +13,12 @@ from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Field
from crispy_forms.bootstrap import PrependedText, AppendedText, PrependedAppendedText, StrictButton, Div
from allauth.account.forms import SignupForm, set_form_field_order
from allauth.account.adapter import DefaultAccountAdapter
from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
from part.models import PartCategory
from common.models import InvenTreeSetting
class HelperForm(forms.ModelForm):
@ -144,7 +149,6 @@ class EditUserForm(HelperForm):
'username',
'first_name',
'last_name',
'email'
]
@ -204,3 +208,76 @@ class SettingCategorySelectForm(forms.ModelForm):
css_class='row',
),
)
# override allauth
class CustomSignupForm(SignupForm):
"""
Override to use dynamic settings
"""
def __init__(self, *args, **kwargs):
kwargs['email_required'] = InvenTreeSetting.get_setting('LOGIN_MAIL_REQUIRED')
super().__init__(*args, **kwargs)
# check for two mail fields
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
self.fields["email2"] = forms.EmailField(
label=_("E-mail (again)"),
widget=forms.TextInput(
attrs={
"type": "email",
"placeholder": _("E-mail address confirmation"),
}
),
)
# check for two password fields
if not InvenTreeSetting.get_setting('LOGIN_SIGNUP_PWD_TWICE'):
self.fields.pop("password2")
# reorder fields
set_form_field_order(self, ["username", "email", "email2", "password1", "password2", ])
def clean(self):
cleaned_data = super().clean()
# check for two mail fields
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_MAIL_TWICE'):
email = cleaned_data.get("email")
email2 = cleaned_data.get("email2")
if (email and email2) and email != email2:
self.add_error("email2", _("You must type the same email each time."))
return cleaned_data
class RegistratonMixin:
"""
Mixin to check if registration should be enabled
"""
def is_open_for_signup(self, request):
if InvenTreeSetting.get_setting('EMAIL_HOST', None) and InvenTreeSetting.get_setting('LOGIN_ENABLE_REG', True):
return super().is_open_for_signup(request)
return False
class CustomAccountAdapter(RegistratonMixin, DefaultAccountAdapter):
"""
Override of adapter to use dynamic settings
"""
def send_mail(self, template_prefix, email, context):
"""only send mail if backend configured"""
if InvenTreeSetting.get_setting('EMAIL_HOST', None):
return super().send_mail(template_prefix, email, context)
return False
class CustomSocialAccountAdapter(RegistratonMixin, DefaultSocialAccountAdapter):
"""
Override of adapter to use dynamic settings
"""
def is_auto_signup_allowed(self, request, sociallogin):
if InvenTreeSetting.get_setting('LOGIN_SIGNUP_SSO_AUTO', True):
return super().is_auto_signup_allowed(request, sociallogin)
return False

View File

@ -64,15 +64,15 @@ class AuthRequiredMiddleware(object):
# No authorization was found for the request
if not authorized:
# A logout request will redirect the user to the login screen
if request.path_info == reverse_lazy('logout'):
return HttpResponseRedirect(reverse_lazy('login'))
if request.path_info == reverse_lazy('account_logout'):
return HttpResponseRedirect(reverse_lazy('account_login'))
path = request.path_info
# List of URL endpoints we *do not* want to redirect to
urls = [
reverse_lazy('login'),
reverse_lazy('logout'),
reverse_lazy('account_login'),
reverse_lazy('account_logout'),
reverse_lazy('admin:login'),
reverse_lazy('admin:logout'),
]
@ -80,7 +80,7 @@ class AuthRequiredMiddleware(object):
if path not in urls and not path.startswith('/api/'):
# Save the 'next' parameter to pass through to the login view
return redirect('%s?next=%s' % (reverse_lazy('login'), request.path))
return redirect('%s?next=%s' % (reverse_lazy('account_login'), request.path))
response = self.get_response(request)

View File

@ -249,6 +249,7 @@ INSTALLED_APPS = [
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.sites',
# InvenTree apps
'build.apps.BuildConfig',
@ -279,6 +280,10 @@ INSTALLED_APPS = [
'error_report', # Error reporting in the admin interface
'django_q',
'formtools', # Form wizard tools
'allauth', # Base app for SSO
'allauth.account', # Extend user with accounts
'allauth.socialaccount', # Use 'social' providers
]
MIDDLEWARE = CONFIG.get('middleware', [
@ -298,7 +303,8 @@ MIDDLEWARE = CONFIG.get('middleware', [
MIDDLEWARE.append('error_report.middleware.ExceptionProcessor')
AUTHENTICATION_BACKENDS = CONFIG.get('authentication_backends', [
'django.contrib.auth.backends.ModelBackend'
'django.contrib.auth.backends.ModelBackend',
'allauth.account.auth_backends.AuthenticationBackend', # SSO login via external providers
])
# If the debug toolbar is enabled, add the modules
@ -646,3 +652,34 @@ MESSAGE_TAGS = {
messages.ERROR: 'alert alert-block alert-danger',
messages.INFO: 'alert alert-block alert-info',
}
SITE_ID = 1
# Load the allauth social backends
SOCIAL_BACKENDS = CONFIG.get('social_backends', [])
for app in SOCIAL_BACKENDS:
INSTALLED_APPS.append(app)
SOCIALACCOUNT_PROVIDERS = CONFIG.get('social_providers', [])
# settings for allauth
ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = get_setting('INVENTREE_LOGIN_CONFIRM_DAYS', CONFIG.get('login_confirm_days', 3))
ACCOUNT_LOGIN_ATTEMPTS_LIMIT = get_setting('INVENTREE_LOGIN_ATTEMPTS', CONFIG.get('login_attempts', 5))
ACCOUNT_LOGOUT_ON_PASSWORD_CHANGE = True
# override forms / adapters
ACCOUNT_FORMS = {
'login': 'allauth.account.forms.LoginForm',
'signup': 'InvenTree.forms.CustomSignupForm',
'add_email': 'allauth.account.forms.AddEmailForm',
'change_password': 'allauth.account.forms.ChangePasswordForm',
'set_password': 'allauth.account.forms.SetPasswordForm',
'reset_password': 'allauth.account.forms.ResetPasswordForm',
'reset_password_from_key': 'allauth.account.forms.ResetPasswordKeyForm',
'disconnect': 'allauth.socialaccount.forms.DisconnectForm',
}
SOCIALACCOUNT_ADAPTER = 'InvenTree.forms.CustomSocialAccountAdapter'
ACCOUNT_ADAPTER = 'InvenTree.forms.CustomAccountAdapter'

View File

@ -111,6 +111,10 @@ class URLTest(TestCase):
if url.startswith("admin:"):
return
# TODO can this be more elegant?
if url.startswith("account_"):
return
if pk:
# We will assume that there is at least one item in the database
reverse(url, kwargs={"pk": 1})

View File

@ -8,7 +8,6 @@ Passes URL lookup downstream to each app as required.
from django.conf.urls import url, include
from django.urls import path
from django.contrib import admin
from django.contrib.auth import views as auth_views
from company.urls import company_urls
from company.urls import manufacturer_part_urls
@ -38,7 +37,7 @@ from rest_framework.documentation import include_docs_urls
from .views import auth_request
from .views import IndexView, SearchView, DatabaseStatsView
from .views import SettingsView, EditUserView, SetPasswordView
from .views import SettingsView, EditUserView, SetPasswordView, CustomEmailView, CustomConnectionsView, CustomPasswordResetFromKeyView
from .views import CurrencyRefreshView
from .views import AppearanceSelectView, SettingCategorySelectView
from .views import DynamicJsView
@ -143,9 +142,6 @@ urlpatterns = [
url(r'^auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^login/?', auth_views.LoginView.as_view(), name='login'),
url(r'^logout/', auth_views.LogoutView.as_view(template_name='registration/logged_out.html'), name='logout'),
url(r'^settings/', include(settings_urls)),
url(r'^edit-user/', EditUserView.as_view(), name='edit-user'),
@ -154,7 +150,6 @@ urlpatterns = [
url(r'^admin/error_log/', include('error_report.urls')),
url(r'^admin/shell/', include('django_admin_shell.urls')),
url(r'^admin/', admin.site.urls, name='inventree-admin'),
url(r'accounts/', include('django.contrib.auth.urls')),
url(r'^index/', IndexView.as_view(), name='index'),
url(r'^search/', SearchView.as_view(), name='search'),
@ -166,6 +161,13 @@ urlpatterns = [
url(r'^api-doc/', include_docs_urls(title='InvenTree API')),
url(r'^markdownx/', include('markdownx.urls')),
# Single Sign On / allauth
# overrides of urlpatterns
url(r'^accounts/email/', CustomEmailView.as_view(), name='account_email'),
url(r'^accounts/social/connections/', CustomConnectionsView.as_view(), name='socialaccount_connections'),
url(r"^accounts/password/reset/key/(?P<uidb36>[0-9A-Za-z]+)-(?P<key>.+)/$", CustomPasswordResetFromKeyView.as_view(), name="account_reset_password_from_key"),
url(r'^accounts/', include('allauth.urls')), # included urlpatterns
]
# Server running in "DEBUG" mode?

View File

@ -17,13 +17,19 @@ from django.urls import reverse_lazy
from django.shortcuts import redirect
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views import View
from django.views.generic import ListView, DetailView, CreateView, FormView, DeleteView, UpdateView
from django.views.generic.base import RedirectView, TemplateView
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
from allauth.account.forms import AddEmailForm
from allauth.socialaccount.forms import DisconnectForm
from allauth.account.models import EmailAddress
from allauth.account.views import EmailView, PasswordResetFromKeyView
from allauth.socialaccount.views import ConnectionsView
from common.settings import currency_code_default, currency_codes
from part.models import Part, PartCategory
@ -810,9 +816,47 @@ class SettingsView(TemplateView):
except:
ctx["locale_stats"] = {}
# Forms and context for allauth
ctx['add_email_form'] = AddEmailForm
ctx["can_add_email"] = EmailAddress.objects.can_add_email(self.request.user)
# Form and context for allauth social-accounts
ctx["request"] = self.request
ctx['social_form'] = DisconnectForm(request=self.request)
return ctx
class AllauthOverrides(LoginRequiredMixin):
"""
Override allauths views to always redirect to success_url
"""
def get(self, request, *args, **kwargs):
# always redirect to settings
return HttpResponseRedirect(self.success_url)
class CustomEmailView(AllauthOverrides, EmailView):
"""
Override of allauths EmailView to always show the settings but leave the functions allow
"""
success_url = reverse_lazy("settings")
class CustomConnectionsView(AllauthOverrides, ConnectionsView):
"""
Override of allauths ConnectionsView to always show the settings but leave the functions allow
"""
success_url = reverse_lazy("settings")
class CustomPasswordResetFromKeyView(PasswordResetFromKeyView):
"""
Override of allauths PasswordResetFromKeyView to always show the settings but leave the functions allow
"""
success_url = reverse_lazy("account_login")
class CurrencyRefreshView(RedirectView):
"""
POST endpoint to refresh / update exchange rates

View File

@ -830,6 +830,50 @@ class InvenTreeSetting(BaseInvenTreeSetting):
'default': True,
'validator': bool,
},
# login / SSO
'LOGIN_ENABLE_PWD_FORGOT': {
'name': _('Enable password forgot'),
'description': _('Enable password forgot function on the login-pages'),
'default': True,
'validator': bool,
},
'LOGIN_ENABLE_REG': {
'name': _('Enable registration'),
'description': _('Enable self-registration for users on the login-pages'),
'default': False,
'validator': bool,
},
'LOGIN_ENABLE_SSO': {
'name': _('Enable SSO'),
'description': _('Enable SSO on the login-pages'),
'default': False,
'validator': bool,
},
'LOGIN_MAIL_REQUIRED': {
'name': _('E-Mail required'),
'description': _('Require user to supply mail on signup'),
'default': False,
'validator': bool,
},
'LOGIN_SIGNUP_SSO_AUTO': {
'name': _('Auto-fill SSO users'),
'description': _('Automatically fill out user-details from SSO account-data'),
'default': True,
'validator': bool,
},
'LOGIN_SIGNUP_MAIL_TWICE': {
'name': _('Mail twice'),
'description': _('On signup ask users twice for their mail'),
'default': False,
'validator': bool,
},
'LOGIN_SIGNUP_PWD_TWICE': {
'name': _('Password twice'),
'description': _('On signup ask users twice for their password'),
'default': True,
'validator': bool,
},
}
class Meta:

View File

@ -141,6 +141,14 @@ static_root: '/home/inventree/data/static'
# - git
# - ssh
# Login configuration
# How long do confirmation mail last?
# Use environment variable INVENTREE_LOGIN_CONFIRM_DAYS
#login_confirm_days: 3
# How many wrong login attempts are permitted?
# Use environment variable INVENTREE_LOGIN_ATTEMPTS
#login_attempts: 5
# Permit custom authentication backends
#authentication_backends:
# - 'django.contrib.auth.backends.ModelBackend'
@ -157,3 +165,14 @@ static_root: '/home/inventree/data/static'
# - 'django.contrib.messages.middleware.MessageMiddleware'
# - 'django.middleware.clickjacking.XFrameOptionsMiddleware'
# - 'InvenTree.middleware.AuthRequiredMiddleware'
# Add SSO login-backends
# social_backends:
# - 'allauth.socialaccount.providers.keycloak'
# Add specific settings
# social_providers:
# keycloak:
# KEYCLOAK_URL: 'https://keycloak.custom/auth'
# KEYCLOAK_REALM: 'master'

View File

@ -351,6 +351,12 @@ def object_link(url_name, pk, ref):
return mark_safe('<b><a href="{}">{}</a></b>'.format(ref_url, ref))
@register.simple_tag()
def mail_configured():
""" Return if mail is configured """
return bool(settings.EMAIL_HOST)
class I18nStaticNode(StaticNode):
"""
custom StaticNode

View File

@ -0,0 +1,31 @@
{% extends "panel.html" %}
{% load i18n %}
{% load inventree_extras %}
{% block label %}login{% endblock %}
{% block heading %}
{% trans "Login Settings" %}
{% endblock %}
{% block content %}
<table class='table table-striped table-condensed'>
{% include "InvenTree/settings/header.html" %}
<tbody>
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_REG" icon="fa-info-circle" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_SSO" icon="fa-info-circle" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_ENABLE_PWD_FORGOT" icon="fa-info-circle" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_MAIL_REQUIRED" icon="fa-info-circle" %}
<tr>
<td>{% trans 'Signup' %}</td>
<td colspan='4'></td>
</tr>
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_MAIL_TWICE" icon="fa-info-circle" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_PWD_TWICE" icon="fa-info-circle" %}
{% include "InvenTree/settings/setting.html" with key="LOGIN_SIGNUP_SSO_AUTO" icon="fa-info-circle" %}
</tbody>
</table>
{% endblock %}

View File

@ -68,6 +68,12 @@
</a>
</li>
<li class='list-group-item' title='{% trans "Login" %}'>
<a href='#' class='nav-toggle' id='select-login'>
<span class='fas fa-fingerprint'></span> {% trans "Login" %}
</a>
</li>
<li class='list-group-item' title='{% trans "Barcodes" %}'>
<a href='#' class='nav-toggle' id='select-barcodes'>
<span class='fas fa-qrcode'></span> {% trans "Barcodes" %}

View File

@ -25,6 +25,7 @@
{% if user.is_staff %}
{% include "InvenTree/settings/global.html" %}
{% include "InvenTree/settings/login.html" %}
{% include "InvenTree/settings/barcode.html" %}
{% include "InvenTree/settings/currencies.html" %}
{% include "InvenTree/settings/report.html" %}

View File

@ -2,6 +2,8 @@
{% load i18n %}
{% load inventree_extras %}
{% load socialaccount %}
{% load crispy_forms_tags %}
{% block label %}account{% endblock %}
@ -10,6 +12,8 @@
{% endblock %}
{% block content %}
{% mail_configured as mail_conf %}
<div class='btn-group' style='float: right;'>
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
@ -32,12 +36,119 @@
<td>{% trans "Last Name" %}</td>
<td>{{ user.last_name }}</td>
</tr>
<tr>
<td>{% trans "Email Address" %}</td>
<td>{{ user.email }}</td>
</tr>
</table>
<div class='panel-heading'>
<h4>{% trans "E-Mail" %}</h4>
</div>
<div>
{% if user.emailaddress_set.all %}
<p>{% trans 'The following e-mail addresses are associated with your account:' %}</p>
<form action="{% url 'account_email' %}" class="email_list" method="post">
{% csrf_token %}
<fieldset class="blockLabels">
{% for emailaddress in user.emailaddress_set.all %}
<div class="ctrlHolder">
<label for="email_radio_{{forloop.counter}}" class="{% if emailaddress.primary %}primary_email{%endif%}">
<input id="email_radio_{{forloop.counter}}" type="radio" name="email" {% if emailaddress.primary or user.emailaddress_set.count == 1 %}checked="checked"{%endif %} value="{{emailaddress.email}}"/>
{{ emailaddress.email }}
{% if emailaddress.verified %}
<span class="verified">{% trans "Verified" %}</span>
{% else %}
<span class="unverified">{% trans "Unverified" %}</span>
{% endif %}
{% if emailaddress.primary %}<span class="primary">{% trans "Primary" %}</span>{% endif %}
</label>
</div>
{% endfor %}
<div class="buttonHolder">
<button class="btn btn-primary secondaryAction" type="submit" name="action_primary" >{% trans 'Make Primary' %}</button>
<button class="btn btn-primary secondaryAction" type="submit" name="action_send" {% if not mail_conf %}disabled{% endif %}>{% trans 'Re-send Verification' %}</button>
<button class="btn btn-primary primaryAction" type="submit" name="action_remove" >{% trans 'Remove' %}</button>
</div>
</fieldset>
</form>
{% else %}
<p><strong>{% trans 'Warning:'%}</strong>
{% trans "You currently do not have any e-mail address set up. You should really add an e-mail address so you can receive notifications, reset your password, etc." %}
</p>
{% endif %}
{% if can_add_email %}
<br>
<h4>{% trans "Add E-mail Address" %}</h4>
<form method="post" action="{% url 'account_email' %}" class="add_email">
{% csrf_token %}
{{ add_email_form|crispy }}
<button class="btn btn-primary" name="action_add" type="submit">{% trans "Add E-mail" %}</button>
</form>
{% endif %}
<br>
</div>
<div class='panel-heading'>
<h4>{% trans "Social Accounts" %}</h4>
</div>
<div>
{% if social_form.accounts %}
<p>{% blocktrans %}You can sign in to your account using any of the following third party accounts:{% endblocktrans %}</p>
<form method="post" action="{% url 'socialaccount_connections' %}">
{% csrf_token %}
<fieldset>
{% if social_form.non_field_errors %}
<div id="errorMsg">{{ social_form.non_field_errors }}</div>
{% endif %}
{% for base_account in social_form.accounts %}
{% with base_account.get_provider_account as account %}
<div>
<label for="id_account_{{ base_account.id }}">
<input id="id_account_{{ base_account.id }}" type="radio" name="account" value="{{ base_account.id }}"/>
<span class="socialaccount_provider {{ base_account.provider }} {{ account.get_brand.id }}">
<span class='brand-icon' brand_name='{{account.get_brand.id}}'></span>{{account.get_brand.name}}</span>
{{ account }}
</label>
</div>
{% endwith %}
{% endfor %}
<div>
<button class="btn btn-primary" type="submit">{% trans 'Remove' %}</button>
</div>
</fieldset>
</form>
{% else %}
<p>{% trans 'You currently have no social network accounts connected to this account.' %}</p>
{% endif %}
<br>
<h4>{% trans 'Add a 3rd Party Account' %}</h4>
<div>
{% include "socialaccount/snippets/provider_list.html" with process="connect" %}
</div>
{% include "socialaccount/snippets/login_extra.html" %}
<br>
</div>
<div class='panel-heading'>
<h4>{% trans "Theme Settings" %}</h4>
</div>
@ -105,4 +216,18 @@
</div>
</div>
{% endblock %}
{% block js_ready %}
(function() {
var message = "{% trans 'Do you really want to remove the selected e-mail address?' %}";
var actions = document.getElementsByName('action_remove');
if (actions.length) {
actions[0].addEventListener("click", function(e) {
if (! confirm(message)) {
e.preventDefault();
}
});
}
})();
{% endblock %}

View File

@ -1,6 +1,5 @@
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load inventree_extras %}
<!DOCTYPE html>
@ -12,57 +11,79 @@
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- CSS -->
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<link rel="stylesheet" href="{% get_color_theme_css user.get_username %}">
<style>
.login-error {
color: #F88;
}
</style>
<title>
{% inventree_title %}
{% inventree_title %} | {% block head_title %}{% endblock %}
</title>
{% block extra_head %}
{% endblock %}
</head>
<body class='login-screen'>
<!--
Background Image Attribution: https://unsplash.com/photos/Ixvv3YZkd7w
-->
<div class='main body-wrapper login-screen'>
<div class='login-container'>
<div class="row">
<div class='container-fluid'>
<div class='clearfix content-heading login-header'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
<span><h3>{% inventree_title %} </h3></span>
</div>
<hr>
<div class='container-fluid'>
<p>{% trans "Forgotten your password?" %}</p>
<p>{% trans "Enter your email address below." %}</p>
<p>{% trans "An email will be sent with password reset instructions." %}</p>
<form method="POST">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary" type="submit">{% trans "Send email" %}</button>
</form>
</div>
<span><h3>{% inventree_title %}</h3></span>
</div>
<hr>
<div class='container-fluid'>{% block content %}{% endblock %}</div>
</div>
</div>
</div>
</div>
{% block extra_body %}
{% endblock %}
</body>
{% include 'notification.html' %}
</div>
<!-- Scripts -->
<script type="text/javascript" src="{% static 'script/jquery_3.3.1_jquery.min.js' %}"></script>
<!-- general InvenTree -->
<script type='text/javascript' src="{% static 'script/inventree/notification.js' %}"></script>
<!-- dynamic javascript templates -->
<script type='text/javascript' src="{% url 'inventree.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<script type='text/javascript'>
$(document).ready(function () {
// notifications
{% if messages %}
{% for message in messages %}
showAlertOrCache('alert-info', '{{message}}', true);
{% endfor %}
{% endif %}
showCachedAlerts();
inventreeDocReady();
});
</script>
</body>
</html>

View File

@ -0,0 +1,31 @@
{% extends "account/base.html" %}
{% load i18n %}
{% load account %}
{% block head_title %}{% trans "Confirm E-mail Address" %}{% endblock %}
{% block content %}
<h1>{% trans "Confirm E-mail Address" %}</h1>
{% if confirmation %}
{% user_display confirmation.email_address.user as user_display %}
<p>{% blocktrans with confirmation.email_address.email as email %}Please confirm that <a href="mailto:{{ email }}">{{ email }}</a> is an e-mail address for user {{ user_display }}.{% endblocktrans %}</p>
<form method="post" action="{% url 'account_confirm_email' confirmation.key %}">
{% csrf_token %}
<button type="submit" class="btn btn-primary btn-block">{% trans 'Confirm' %}</button>
</form>
{% else %}
{% url 'account_email' as email_url %}
<p>{% blocktrans %}This e-mail confirmation link expired or is invalid. Please <a href="{{ email_url }}">issue a new e-mail confirmation request</a>.{% endblocktrans %}</p>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,52 @@
{% extends "account/base.html" %}
{% load i18n account socialaccount crispy_forms_tags inventree_extras %}
{% block head_title %}{% trans "Sign In" %}{% endblock %}
{% block content %}
{% settings_value 'LOGIN_ENABLE_REG' as enable_reg %}
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
{% mail_configured as mail_conf %}
<h1>{% trans "Sign In" %}</h1>
{% if enable_reg %}
{% get_providers as socialaccount_providers %}
{% if socialaccount_providers %}
<p>{% blocktrans with site.name as site_name %}Please sign in with one
of your existing third party accounts or <a class="btn btn-primary btn-small" href="{{ signup_url }}">sign up</a>
for a account and sign in below:{% endblocktrans %}</p>
{% else %}
<p>{% blocktrans %}If you have not created an account yet, then please
<a href="{{ signup_url }}">sign up</a> first.{% endblocktrans %}</p>
{% endif %}
{% endif %}
<form class="login" method="POST" action="{% url 'account_login' %}">
{% csrf_token %}
{{ form|crispy }}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<div class="btn-toolbar">
<button class="btn btn-primary col-md-8" type="submit">{% trans "Sign In" %}</button>
{% if mail_conf and enable_pwd_forgot %}
<a class="btn btn-primary" href="{% url 'account_reset_password' %}">{% trans "Forgot Password?" %}</a>
{% endif %}
</div>
</form>
{% if enable_sso %}
<br>
<h4 class="text-center">{% trans 'or use SSO' %}</h4>
<div>
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
</div>
{% include "socialaccount/snippets/login_extra.html" %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,21 @@
{% extends "account/base.html" %}
{% load i18n %}
{% block head_title %}{% trans "Sign Out" %}{% endblock %}
{% block content %}
<h1>{% trans "Sign Out" %}</h1>
<p>{% trans 'Are you sure you want to sign out?' %}</p>
<form method="post" action="{% url 'account_logout' %}">
{% csrf_token %}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}"/>
{% endif %}
<button type="submit" class="btn btn-primary btn-block">{% trans 'Sign Out' %}</button>
</form>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "account/base.html" %}
{% load i18n account crispy_forms_tags inventree_extras %}
{% block head_title %}{% trans "Password Reset" %}{% endblock %}
{% block content %}
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
{% mail_configured as mail_conf %}
<h1>{% trans "Password Reset" %}</h1>
{% if user.is_authenticated %}
{% include "account/snippets/already_logged_in.html" %}
{% endif %}
{% if mail_conf and enable_pwd_forgot %}
<p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll send you an e-mail allowing you to reset it." %}</p>
<form method="POST" action="{% url 'account_reset_password' %}" class="password_reset">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary btn-block" value="{% trans 'Reset My Password' %}" />
</form>
{% else %}
<div class='alert alert-block alert-danger'>
<p>{% trans "This function is currently disabled. Please contact an administrator." %}</p>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "account/base.html" %}
{% load i18n crispy_forms_tags %}
{% block head_title %}{% trans "Change Password" %}{% endblock %}
{% block content %}
<h1>{% if token_fail %}{% trans "Bad Token" %}{% else %}{% trans "Change Password" %}{% endif %}</h1>
{% if token_fail %}
{% url 'account_reset_password' as passwd_reset_url %}
<p>{% blocktrans %}The password reset link was invalid, possibly because it has already been used. Please request a <a href="{{ passwd_reset_url }}">new password reset</a>.{% endblocktrans %}</p>
{% else %}
{% if form %}
<form method="POST" action="{{ action_url }}">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" name="action" class="btn btn-primary btn-block" value="{% trans 'change password' %}"/>
</form>
{% else %}
<p>{% trans 'Your password is now changed.' %}</p>
{% endif %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,40 @@
{% extends "account/base.html" %}
{% load i18n crispy_forms_tags inventree_extras %}
{% block head_title %}{% trans "Signup" %}{% endblock %}
{% block content %}
{% settings_value 'LOGIN_ENABLE_REG' as enable_reg %}
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
<h1>{% trans "Sign Up" %}</h1>
<p>{% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktrans %}</p>
{% if enable_reg %}
<form class="signup" id="signup_form" method="post" action="{% url 'account_signup' %}">
{% csrf_token %}
{{ form|crispy }}
{% if redirect_field_value %}
<input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" />
{% endif %}
<button type="submit" class="btn btn-primary btn-block">{% trans "Sign Up" %}</button>
</form>
{% if enable_sso %}
<br>
<h4>{% trans 'Or use a SSO-provider for signup' %}</h4>
<div>
{% include "socialaccount/snippets/provider_list.html" with process="login" %}
</div>
{% include "socialaccount/snippets/login_extra.html" %}
{% endif %}
{% else %}
<div class='alert alert-block alert-danger'>
<p>{% trans "This function is currently disabled. Please contact an administrator." %}</p>
</div>
{% endif %}
{% endblock %}

View File

@ -177,6 +177,11 @@ function inventreeDocReady() {
'ui-autocomplete': 'dropdown-menu search-menu',
},
});
// Generate brand-icons
$('.brand-icon').each(function(i, obj) {
loadBrandIcon($(this), $(this).attr('brand_name'));
});
}
function isFileTransfer(transfer) {
@ -275,3 +280,13 @@ function inventreeLoad(name, defaultValue) {
return value;
}
}
function loadBrandIcon(element, name) {
// check if icon exists
var icon = window.FontAwesome.icon({prefix: 'fab', iconName: name});
if (icon) {
// add icon to button
element.addClass('fab fa-' + name);
}
}

View File

@ -82,9 +82,9 @@
{% if user.is_staff %}
<li><a href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
{% endif %}
<li><a href="{% url 'logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
<li><a href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>
{% else %}
<li><a href="{% url 'login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li>
<li><a href="{% url 'account_login' %}"><span class="fas fa-sign-in-alt"></span> {% trans "Login" %}</a></li>
{% endif %}
<hr>
<li><a href="{% url 'settings' %}"><span class="fas fa-cog"></span> {% trans "Settings" %}</a></li>

View File

@ -1,59 +1,10 @@
{% load static %}
{% extends "registration/logged_out.html" %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load inventree_extras %}
<!DOCTYPE html>
<html lang="en">
<head>
{% block content %}
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<p>{% translate "You were logged out successfully." %}</p>
<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
<p><a href="{% url 'admin:index' %}">{% translate 'Log in again' %}</a></p>
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<style>
.login-error {
color: #F88;
}
</style>
<title>
{% inventree_title %}
</title>
</head>
<body class='login-screen'>
<div class='main body-wrapper login-screen'>
<div class='login-container'>
<div class="row">
<div class='container-fluid'>
<div class='clearfix content-heading login-header'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
<span><h3>{% inventree_title %} </h3></span>
</div>
<hr>
<div class='container-fluid'>
<p>{% trans "You have been logged out" %}</p>
<p><a href='{% url "login" %}'>{% trans "Return to login screen" %}</a></p>
</div>
</div>
</div>
</div>
</div>
</body>
{% endblock %}

View File

@ -1,105 +0,0 @@
{% load static %}
{% load i18n %}
{% load inventree_extras %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<style>
.login-error {
color: #F88;
}
</style>
<title>
{% inventree_title %}
</title>
</head>
<body class='login-screen'>
<!--
Background Image Attribution: https://unsplash.com/photos/Ixvv3YZkd7w
-->
<div class='main body-wrapper login-screen'>
<div class='login-container'>
<div class="row">
<div class='container-fluid'>
<div class='clearfix content-heading login-header'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
<span><h3>{% inventree_title %} </h3></span>
</div>
<hr>
<div class='container-fluid'>
<form method="post" action=''>
{% csrf_token %}
{% load crispy_forms_tags %}
<div id="div_id_username" class="form-group">
<label for="id_username" class="control-label requiredField">{% trans "Username" %}<span class="asteriskField">*</span></label>
<div class="controls ">
<div class='input-group'>
<div class='input-group-addon'>
<span class='fas fa-user'></span>
</div>
<input type="text" name="username" autofocus autocapitalize="none" autocomplete="username" maxlength="150" class="textinput textInput form-control" required id="id_username" placeholder='{% trans "Enter username" %}'>
</div>
</div>
</div>
<div id="div_id_password" class="form-group">
<label for="id_password" class="control-label requiredField">{% trans "Password" %}<span class="asteriskField">*</span></label>
<div class='controls'>
<div class="input-group">
<div class='input-group-addon'>
<span class='fas fa-key'></span>
</div>
<input type="password" name="password" autocomplete="current-password" class="textinput textInput form-control" required id="id_password" placeholder='{% trans "Enter password" %}'>
</div>
</div>
</div>
{% if form.errors %}
<div class='login-error'>
<strong>{% trans "Username / password combination is incorrect" %}</strong>
</div>
{% endif %}
<hr>
<button class='pull-right btn btn-primary login-button' type="submit">{% trans "Login" %}</button>
</form>
{% if email_configured %}
<hr><br>
<p>{% trans "Forgotten your password?" %} - <a href='{% url "password_reset" %}'>{% trans "Click here to reset" %}</a></p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,59 +0,0 @@
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load inventree_extras %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<style>
.login-error {
color: #F88;
}
</style>
<title>
{% inventree_title %}
</title>
</head>
<body class='login-screen'>
<div class='main body-wrapper login-screen'>
<div class='login-container'>
<div class="row">
<div class='container-fluid'>
<div class='clearfix content-heading login-header'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
<span><h3>{% inventree_title %} </h3></span>
</div>
<hr>
<div class='container-fluid'>
<p>{% trans "Password reset complete" %}</p>
<p><a href='{% url "login" %}'>{% trans "Return to login screen" %}</a></p>
</div>
</div>
</div>
</div>
</div>
</body>

View File

@ -1,69 +0,0 @@
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load inventree_extras %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<style>
.login-error {
color: #F88;
}
</style>
<title>
{% inventree_title %}
</title>
</head>
<body class='login-screen'>
<div class='main body-wrapper login-screen'>
<div class='login-container'>
<div class="row">
<div class='container-fluid'>
<div class='clearfix content-heading login-header'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
<span><h3>{% inventree_title %} </h3></span>
</div>
<hr>
<div class='container-fluid'>
{% if validlink %}
<h3>{% trans "Change password" %}</h3>
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button class="btn btn-primary" type="submit">{% trans "Change password" %}</button>
</form>
{% else %}
<p>
{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}
</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</body>

View File

@ -1,65 +0,0 @@
{% load static %}
{% load i18n %}
{% load crispy_forms_tags %}
{% load inventree_extras %}
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Required meta tags -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- CSS -->
<link rel="stylesheet" href="{% static 'css/bootstrap_3.3.7_css_bootstrap.min.css' %}">
<link rel="stylesheet" href="{% static 'select2/css/select2.css' %}">
<link rel="stylesheet" href="{% static 'css/inventree.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/brands.css' %}">
<link rel="stylesheet" href="{% static 'fontawesome/css/solid.css' %}">
<script type='text/javascript' src="{% static 'fontawesome/js/solid.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/brands.js' %}"></script>
<script type='text/javascript' src="{% static 'fontawesome/js/fontawesome.js' %}"></script>
<style>
.login-error {
color: #F88;
}
</style>
<title>
{% inventree_title %}
</title>
</head>
<body class='login-screen'>
<div class='main body-wrapper login-screen'>
<div class='login-container'>
<div class="row">
<div class='container-fluid'>
<div class='clearfix content-heading login-header'>
<img class="pull-left" src="{% static 'img/inventree.png' %}" width="60" height="60"/>
<span><h3>{% inventree_title %} </h3></span>
</div>
<hr>
<div class='container-fluid'>
<p>
{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}
</p>
<p>
{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}
</p>
<hr>
<a href='{% url "login" %}'>{% trans "Return to login screen" %}</a>
</div>
</div>
</div>
</div>
</div>
</body>

View File

@ -0,0 +1,17 @@
{% load socialaccount %}
{% get_providers as socialaccount_providers %}
{% for provider in socialaccount_providers %}
{% if provider.id == "openid" %}
{% for brand in provider.get_brands %}
<a title="{{brand.name}}"
class="btn btn-primary socialaccount_provider {{provider.id}} {{brand.id}}"
href="{% provider_login_url provider.id openid=brand.openid_url process=process %}"
><span class='brand-icon' brand_name='{{provider.id}}'></span> {{brand.name}}</a>
{% endfor %}
{% endif %}
<a title="{{provider.name}}" class="btn btn-primary socialaccount_provider {{provider.id}}"
href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}"
><span class='brand-icon' brand_name='{{provider.id}}'></span> {{provider.name}}</a>
{% endfor %}

View File

@ -67,7 +67,12 @@ class RuleSet(models.Model):
'report_billofmaterialsreport',
'report_purchaseorderreport',
'report_salesorderreport',
'account_emailaddress',
'account_emailconfirmation',
'sites_site',
'socialaccount_socialaccount',
'socialaccount_socialapp',
'socialaccount_socialtoken',
],
'part_category': [
'part_partcategory',

View File

@ -68,7 +68,9 @@ RUN apk add --no-cache git make bash \
# PostgreSQL support
postgresql postgresql-contrib postgresql-dev libpq \
# MySQL/MariaDB support
mariadb-connector-c mariadb-dev mariadb-client
mariadb-connector-c mariadb-dev mariadb-client \
# Required for python cryptography support
rust cargo
# Install required base-level python packages
COPY requirements.txt requirements.txt

View File

@ -35,5 +35,6 @@ python-barcode[images]==0.13.1 # Barcode generator
qrcode[pil]==6.1 # QR code generator
django-q==1.3.4 # Background task scheduling
django-formtools==2.3 # Form wizard tools
django-allauth==0.45.0 # SSO for external providers via OpenID
inventree # Install the latest version of the InvenTree API python library