2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-08-08 21:00:55 +00:00

[FR] Two-Factor Authentication

Fixes 
This commit is contained in:
Matthias
2021-11-19 23:48:12 +01:00
69 changed files with 20388 additions and 2160 deletions

@@ -12,6 +12,7 @@
<table class='table table-striped table-condensed'>
<tbody>
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE" %}
{% include "InvenTree/settings/setting.html" with key="REPORT_DEFAULT_PAGE_SIZE" %}
{% include "InvenTree/settings/setting.html" with key="REPORT_DEBUG_MODE" %}
{% include "InvenTree/settings/setting.html" with key="REPORT_ENABLE_TEST_REPORT" %}

@@ -4,6 +4,9 @@
{% load static %}
{% load inventree_extras %}
{% block breadcrumb_list %}
{% endblock %}
{% block page_title %}
{% inventree_title %} | {% trans "Settings" %}
{% endblock %}

@@ -12,12 +12,15 @@
{% endblock %}
{% block actions %}
{% inventree_demo_mode as demo %}
{% if not demo %}
<div class='btn btn-primary' type='button' id='edit-user' title='{% trans "Edit User Information" %}'>
<span class='fas fa-user-cog'></span> {% trans "Edit" %}
</div>
<div class='btn btn-primary' type='button' id='edit-password' title='{% trans "Change Password" %}'>
<span class='fas fa-key'></span> {% trans "Set Password" %}
</div>
{% endif %}
{% endblock %}
{% block content %}
@@ -235,7 +238,6 @@
</div>
</form>
</div>
</div>
<div class="row">
<div class='panel-heading'>

@@ -21,4 +21,33 @@
</table>
</div>
<div class='panel-heading'>
<h4>{% trans "Theme Settings" %}</h4>
</div>
<div class='row'>
<div class='col-sm-6'>
<form action='{% url "settings-appearance" %}' method='post'>
{% csrf_token %}
<input name='next' type='hidden' value='{% url "settings" %}'>
<label for='theme' class=' requiredField'>
{% trans "Select theme" %}
</label>
<div class='form-group input-group mb-3'>
<select id='theme' name='theme' class='select form-control'>
{% get_available_themes as themes %}
{% get_user_color_theme request.user.username as user_theme %}
{% for theme in themes %}
<option value='{{ theme.key }}'{% if theme.key == user_theme %} selected{% endif%}>{{ theme.name }}</option>
{% endfor %}
</select>
<div class='input-group-append'>
<input type="submit" value="{% trans 'Set Theme' %}" class="btn btn-primary">
</div>
</div>
</form>
</div>
</div>
{% endblock %}

@@ -1,5 +1,6 @@
{% extends "account/base.html" %}
{% load inventree_extras %}
{% load i18n account socialaccount crispy_forms_tags inventree_extras %}
{% block head_title %}{% trans "Sign In" %}{% endblock %}
@@ -10,6 +11,7 @@
{% settings_value 'LOGIN_ENABLE_PWD_FORGOT' as enable_pwd_forgot %}
{% settings_value 'LOGIN_ENABLE_SSO' as enable_sso %}
{% mail_configured as mail_conf %}
{% inventree_demo_mode as demo %}
<h1>{% trans "Sign In" %}</h1>
@@ -36,9 +38,16 @@ for a account and sign in below:{% endblocktrans %}</p>
<div class="btn-group float-right" role="group">
<button class="btn btn-success" type="submit">{% trans "Sign In" %}</button>
</div>
{% if mail_conf and enable_pwd_forgot %}
{% if mail_conf and enable_pwd_forgot and not demo %}
<a class="" href="{% url 'account_reset_password' %}"><small>{% trans "Forgot Password?" %}</small></a>
{% endif %}
{% if demo %}
<p>
<h6>
{% trans "InvenTree demo instance" %} - <a href='https://inventree.readthedocs.io/en/latest/demo/'>{% trans "Click here for login details" %}</a>
</h6>
</p>
{% endif %}
</form>
{% if enable_sso %}

@@ -4,6 +4,8 @@
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'REPORT_ENABLE_TEST_REPORT' as test_report_enabled %}
{% settings_value "REPORT_ENABLE" as report_enabled %}
{% settings_value "SERVER_RESTART_REQUIRED" as server_restart_required %}
<!DOCTYPE html>
<html lang="en">
@@ -84,10 +86,20 @@
</div>
</div>
<main class='col ps-md-2 pt-2 pe-2'>
{% block alerts %}
<div class='notification-area' id='alerts'>
<!-- Div for displayed alerts -->
{% if server_restart_required %}
<div id='alert-restart-server' class='alert alert-danger' role='alert'>
<span class='fas fa-server'></span>
<b>{% trans "Server Restart Required" %}</b>
<small>
<br>
{% trans "A configuration option has been changed which requires a server restart" %}. {% trans "Contact your system administrator for further information" %}
</small>
</div>
{% endif %}
</div>
{% endblock %}

@@ -13,7 +13,7 @@ const user_settings = {
{% endfor %}
};
{% global_settings as GLOBAL_SETTINGS %}
{% visible_global_settings as GLOBAL_SETTINGS %}
const global_settings = {
{% for key, value in GLOBAL_SETTINGS.items %}
{{ key }}: {% primitive_to_javascript value %},

@@ -217,8 +217,10 @@ function showApiError(xhr, url) {
break;
}
message += '<hr>';
message += `URL: ${url}`;
if (url) {
message += '<hr>';
message += `URL: ${url}`;
}
showMessage(title, {
style: 'danger',

@@ -16,6 +16,7 @@
/* exported
newPartFromBomWizard,
loadBomTable,
loadUsedInTable,
removeRowFromBomWizard,
removeColFromBomWizard,
*/
@@ -311,7 +312,7 @@ function bomSubstitutesDialog(bom_item_id, substitutes, options={}) {
}
function loadBomTable(table, options) {
function loadBomTable(table, options={}) {
/* Load a BOM table with some configurable options.
*
* Following options are available:
@@ -395,7 +396,7 @@ function loadBomTable(table, options) {
var sub_part = row.sub_part_detail;
html += makePartIcons(row.sub_part_detail);
html += makePartIcons(sub_part);
if (row.substitutes && row.substitutes.length > 0) {
html += makeIconBadge('fa-exchange-alt', '{% trans "Substitutes Available" %}');
@@ -672,8 +673,9 @@ function loadBomTable(table, options) {
table.treegrid('collapseAll');
},
error: function() {
error: function(xhr) {
console.log('Error requesting BOM for part=' + part_pk);
showApiError(xhr);
}
}
);
@@ -835,3 +837,166 @@ function loadBomTable(table, options) {
});
}
}
/*
* Load a table which shows the assemblies which "require" a certain part.
*
* Arguments:
* - table: The ID string of the table element e.g. '#used-in-table'
* - part_id: The ID (PK) of the part we are interested in
*
* Options:
* -
*
* The following "options" are available.
*/
function loadUsedInTable(table, part_id, options={}) {
var params = options.params || {};
params.uses = part_id;
params.part_detail = true;
params.sub_part_detail = true,
params.show_pricing = global_settings.PART_SHOW_PRICE_IN_BOM;
var filters = {};
if (!options.disableFilters) {
filters = loadTableFilters('usedin');
}
for (var key in params) {
filters[key] = params[key];
}
setupFilterList('usedin', $(table), options.filterTarget || '#filter-list-usedin');
function loadVariantData(row) {
// Load variants information for inherited BOM rows
inventreeGet(
'{% url "api-part-list" %}',
{
assembly: true,
ancestor: row.part,
},
{
success: function(variantData) {
// Iterate through each variant item
for (var jj = 0; jj < variantData.length; jj++) {
variantData[jj].parent = row.pk;
var variant = variantData[jj];
// Add this variant to the table, augmented
$(table).bootstrapTable('append', [{
// Point the parent to the "master" assembly row
parent: row.pk,
part: variant.pk,
part_detail: variant,
sub_part: row.sub_part,
sub_part_detail: row.sub_part_detail,
quantity: row.quantity,
}]);
}
},
error: function(xhr) {
showApiError(xhr);
}
}
);
}
$(table).inventreeTable({
url: options.url || '{% url "api-bom-list" %}',
name: options.table_name || 'usedin',
sortable: true,
search: true,
showColumns: true,
queryParams: filters,
original: params,
rootParentId: 'top-level-item',
idField: 'pk',
uniqueId: 'pk',
parentIdField: 'parent',
treeShowField: 'part',
onLoadSuccess: function(tableData) {
// Once the initial data are loaded, check if there are any "inherited" BOM lines
for (var ii = 0; ii < tableData.length; ii++) {
var row = tableData[ii];
// This is a "top level" item in the table
row.parent = 'top-level-item';
// Ignore this row as it is not "inherited" by variant parts
if (!row.inherited) {
continue;
}
loadVariantData(row);
}
},
onPostBody: function() {
$(table).treegrid({
treeColumn: 0,
});
},
columns: [
{
field: 'pk',
title: 'ID',
visible: false,
switchable: false,
},
{
field: 'part',
title: '{% trans "Assembly" %}',
switchable: false,
sortable: true,
formatter: function(value, row) {
var url = `/part/${value}/?display=bom`;
var html = '';
var part = row.part_detail;
html += imageHoverIcon(part.thumbnail);
html += renderLink(part.full_name, url);
html += makePartIcons(part);
return html;
}
},
{
field: 'sub_part',
title: '{% trans "Required Part" %}',
sortable: true,
formatter: function(value, row) {
var url = `/part/${value}/`;
var html = '';
var sub_part = row.sub_part_detail;
html += imageHoverIcon(sub_part.thumbnail);
html += renderLink(sub_part.full_name, url);
html += makePartIcons(sub_part);
return html;
}
},
{
field: 'quantity',
title: '{% trans "Required Quantity" %}',
formatter: function(value, row) {
var html = value;
if (row.parent && row.parent != 'top-level-item') {
html += ` <em>({% trans "Inherited from parent BOM" %})</em>`;
}
return html;
}
}
]
});
}

@@ -281,23 +281,24 @@ function setupFilterList(tableKey, table, target) {
// One blank slate, please
element.empty();
element.append(`<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`);
var buttons = '';
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
buttons += `<button id='reload-${tableKey}' title='{% trans "Reload data" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-redo-alt'></span></button>`;
// If there are no filters defined for this table, exit now
if (jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
return;
// If there are filters defined for this table, add more buttons
if (!jQuery.isEmptyObject(getAvailableTableFilters(tableKey))) {
buttons += `<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-filter'></span></button>`;
if (Object.keys(filters).length > 0) {
buttons += `<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-backspace icon-red'></span></button>`;
}
}
element.append(`<button id='${add}' title='{% trans "Add new filter" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-filter'></span></button>`);
if (Object.keys(filters).length > 0) {
element.append(`<button id='${clear}' title='{% trans "Clear all filters" %}' class='btn btn-outline-secondary filter-button'><span class='fas fa-backspace icon-red'></span></button>`);
}
element.html(`
<div class='btn-group' role='group'>
${buttons}
</div>
`);
for (var key in filters) {
var value = getFilterOptionValue(tableKey, key, filters[key]);
@@ -307,6 +308,11 @@ function setupFilterList(tableKey, table, target) {
element.append(`<div title='${description}' class='filter-tag'>${title} = ${value}<span ${tag}='${key}' class='close'>x</span></div>`);
}
// Callback for reloading the table
element.find(`#reload-${tableKey}`).click(function() {
$(table).bootstrapTable('refresh');
});
// Add a callback for adding a new filter
element.find(`#${add}`).click(function clicked() {
@@ -316,10 +322,12 @@ function setupFilterList(tableKey, table, target) {
var html = '';
html += `<div class='input-group'>`;
html += generateAvailableFilterList(tableKey);
html += generateFilterInput(tableKey);
html += `<button title='{% trans "Create filter" %}' class='btn btn-outline-secondary filter-button' id='${make}'><span class='fas fa-plus'></span></button>`;
html += `</div>`;
element.append(html);

@@ -924,8 +924,8 @@ function handleFormSuccess(response, options) {
var cache = (options.follow && response.url) || options.redirect || options.reload;
// Display any messages
if (response && response.success) {
showAlertOrCache(response.success, cache, {style: 'success'});
if (response && (response.success || options.successMessage)) {
showAlertOrCache(response.success || options.successMessage, cache, {style: 'success'});
}
if (response && response.info) {

@@ -331,6 +331,7 @@ function editPart(pk) {
groups: groups,
title: '{% trans "Edit Part" %}',
reload: true,
successMessage: '{% trans "Part edited" %}',
});
}

@@ -1128,7 +1128,9 @@ function loadStockTable(table, options) {
col = {
field: 'quantity',
sortName: 'stock',
title: '{% trans "Stock" %}',
sortable: true,
formatter: function(value, row) {
var val = parseFloat(value);

@@ -77,10 +77,22 @@ function getAvailableTableFilters(tableKey) {
// Filters for the "used in" table
if (tableKey == 'usedin') {
return {
'inherited': {
type: 'bool',
title: '{% trans "Inherited" %}',
},
'optional': {
type: 'bool',
title: '{% trans "Optional" %}',
},
'part_active': {
type: 'bool',
title: '{% trans "Active" %}',
},
'part_trackable': {
type: 'bool',
title: '{% trans "Trackable" %}',
},
};
}

@@ -4,6 +4,7 @@
{% settings_value 'BARCODE_ENABLE' as barcodes %}
{% settings_value 'STICKY_HEADER' user=request.user as sticky %}
{% inventree_demo_mode as demo %}
<nav class="navbar {% if sticky %}fixed-top{% endif %} navbar-expand-lg navbar-light">
<div class="container-fluid">
@@ -58,6 +59,9 @@
{% endif %}
</ul>
</div>
{% if demo %}
{% include "navbar_demo.html" %}
{% endif %}
{% include "search_form.html" %}
<ul class='navbar-nav flex-row'>
{% if barcodes %}
@@ -78,7 +82,7 @@
</a>
<ul class='dropdown-menu dropdown-menu-end inventree-navbar-menu'>
{% if user.is_authenticated %}
{% if user.is_staff %}
{% if user.is_staff and not demo %}
<li><a class='dropdown-item' href="/admin/"><span class="fas fa-user"></span> {% trans "Admin" %}</a></li>
{% endif %}
<li><a class='dropdown-item' href="{% url 'account_logout' %}"><span class="fas fa-sign-out-alt"></span> {% trans "Logout" %}</a></li>

@@ -0,0 +1,12 @@
{% load i18n %}
{% include "spacer.html" %}
<div class='flex'>
<h6>
{% trans "InvenTree demo mode" %}
<a href='https://inventree.readthedocs.io/en/latest/demo/'>
<span class='fas fa-info-circle'></span>
</a>
</h6>
</div>
{% include "spacer.html" %}
{% include "spacer.html" %}

@@ -2,8 +2,10 @@
<form class="d-flex" action="{% url 'search' %}" method='post'>
{% csrf_token %}
<input type="text" name='search' class="form-control" aria-label='{% trans "Search" %}' id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
<button type="submit" id='search-submit' class="btn btn-secondary" title='{% trans "Search" %}'>
<span class='fas fa-search'></span>
</button>
<div class='input-group'>
<input type="text" name='search' class="form-control" aria-label='{% trans "Search" %}' id="search-bar" placeholder="{% trans 'Search' %}"{% if query_text %} value="{{ query }}"{% endif %}>
<button type="submit" id='search-submit' class="btn btn-secondary" title='{% trans "Search" %}'>
<span class='fas fa-search'></span>
</button>
</div>
</form>

@@ -1,7 +1,7 @@
{% load i18n %}
<a href="#" id='select-{{ label }}' title='{% trans text %}' class="list-group-item sidebar-list-group-item border-end-0 d-inline-block text-truncate sidebar-selector" data-bs-parent="#sidebar">
<i class="bi bi-bootstrap"></i>
<span class='sidebar-item-icon fas {{ icon }}'></span>
<span class='sidebar-item-icon fas {{ icon|default:"fa-circle" }}'></span>
<span class='sidebar-item-text' style='display: none;'>{% trans text %}</span>
{% if badge %}
<span id='sidebar-badge-{{ label }}' class='sidebar-item-badge badge rounded-pill badge-right bg-dark'>

@@ -27,6 +27,9 @@ function {{ label }}StatusDisplay(key, options={}) {
label = {{ label }}Codes[key].label;
}
// Fallback option for label
label = label || 'bg-dark';
if (value == null || value.length == 0) {
value = key;
label = '';