Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • uffd/uffd
  • rixx/uffd
  • thies/uffd
  • leona/uffd
  • enbewe/uffd
  • strifel/uffd
  • thies/uffd-2
7 results
Select Git revision
Show changes
Showing
with 1557 additions and 0 deletions
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for("selfservice.token_password", token_id=token.id, token=token.token) }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '') ">
<div class="col-12">
<h2 class="text-center">{{_("Reset password")}}</h2>
</div>
<div class="form-group col-12">
<label for="user-password1">{{_("New Password")}}</label>
<input type="password" autocomplete="new-password" class="form-control" id="user-password1" name="password1" tabindex="2" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}" required>
<small class="form-text text-muted">
{{ User.PASSWORD_DESCRIPTION|safe }}
</small>
</div>
<div class="form-group col-12">
<label for="user-password2">{{_("Repeat Password")}}</label>
<input type="password" autocomplete="new-password" class="form-control" id="user-password2" name="password2" tabindex="3" required>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex="3">{{_("Set password")}}</button>
</div>
</form>
{% endblock %}
...@@ -8,27 +8,32 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -8,27 +8,32 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
#} #}
{% set mfa_enabled = totp_methods or webauthn_methods %} {% set mfa_enabled = request.user.mfa_enabled %}
{% set mfa_init = not recovery_methods and not mfa_enabled %} {% set mfa_init = not request.user.mfa_recovery_codes and not mfa_enabled %}
{% set mfa_setup = recovery_methods and not mfa_enabled %} {% set mfa_setup = request.user.mfa_recovery_codes and not mfa_enabled %}
{% block body %} {% block body %}
<p>Two-factor authentication is currently <strong>{{ 'enabled' if mfa_enabled else 'disabled' }}</strong>. <p>
{% if mfa_enabled %}
{{ _("Two-factor authentication is currently <strong>enabled</strong>.")|safe }}
{% else %}
{{ _("Two-factor authentication is currently <strong>disabled</strong>.")|safe }}
{% endif %}
{% if mfa_init %} {% if mfa_init %}
You need to generate recovery codes and setup at least one authentication method to enable two-factor authentication. {{_("You need to generate recovery codes and setup at least one authentication method to enable two-factor authentication.")}}
{% elif mfa_setup %} {% elif mfa_setup %}
You need to setup at least one authentication method to enable two-factor authentication. {{_("You need to setup at least one authentication method to enable two-factor authentication.")}}
{% endif %} {% endif %}
</p> </p>
{% if mfa_setup or mfa_enabled %} {% if mfa_setup or mfa_enabled %}
<div class="clearfix"> <div class="clearfix">
{% if mfa_enabled %} {% if mfa_enabled %}
<form class="form float-right" action="{{ url_for('mfa.disable') }}"> <form class="form float-right" action="{{ url_for('selfservice.disable_mfa') }}">
<button type="submit" class="btn btn-danger mb-2">Disable two-factor authentication</button> <button type="submit" class="btn btn-danger mb-2">{{_("Disable two-factor authentication")}}</button>
</form> </form>
{% else %} {% else %}
<form class="form float-right" action="{{ url_for('mfa.disable_confirm') }}" method="POST"> <form class="form float-right" action="{{ url_for('selfservice.disable_mfa_confirm') }}" method="POST">
<button type="submit" class="btn btn-light mb-2">Reset two-factor configuration</button> <button type="submit" class="btn btn-light mb-2">{{_("Reset two-factor configuration")}}</button>
</form> </form>
{% endif %} {% endif %}
</div> </div>
...@@ -38,29 +43,37 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -38,29 +43,37 @@ You need to setup at least one authentication method to enable two-factor authen
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12 col-md-5"> <div class="col-12 col-md-5">
<h4>Recovery Codes</h4> <h4>{{_("Recovery Codes")}}</h4>
<p>Recovery codes allow you to login and setup new two-factor methods when you lost your registered second factor.</p> <p>
{{_("Recovery codes allow you to login and setup new two-factor methods when you lost your registered second factor.")}}
</p>
<p> <p>
{% if mfa_init %}<strong>{% endif %} {% if mfa_init %}<strong>{% endif %}
You need to setup recovery codes before you can setup up authenticator apps or U2F/FIDO2 devices. {{_("You need to setup recovery codes before you can setup up authenticator apps or U2F/FIDO2 devices.")}}
{% if mfa_init %}</strong>{% endif %} {% if mfa_init %}</strong>{% endif %}
Each code can only be used once. {{_("Each code can only be used once.")}}
</p> </p>
</div> </div>
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
<form class="form" action="{{ url_for('mfa.setup_recovery') }}" method="POST"> <form class="form" action="{{ url_for('selfservice.setup_mfa_recovery') }}" method="POST">
{% if mfa_init %} {% if mfa_init %}
<button type="submit" class="btn btn-primary mb-2 col">Generate recovery codes to enable two-factor authentication</button> <button type="submit" class="btn btn-primary mb-2 col">
{{_("Generate recovery codes to enable two-factor authentication")}}
</button>
{% else %} {% else %}
<button type="submit" class="btn btn-primary mb-2 col">Generate new recovery codes</button> <button type="submit" class="btn btn-primary mb-2 col">
{{_("Generate new recovery codes")}}
</button>
{% endif %} {% endif %}
</form> </form>
{% if recovery_methods %} {% if request.user.mfa_recovery_codes %}
<p>{{ recovery_methods|length }} recovery codes remain</p> <p>{{ request.user.mfa_recovery_codes|length }} recovery codes remain</p>
{% elif not recovery_methods and mfa_enabled %} {% elif not request.user.mfa_recovery_codes and mfa_enabled %}
<p><strong>You have no remaining recovery codes.</strong></p> <p>
<strong>{{_("You have no remaining recovery codes.")}}</strong>
</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
...@@ -69,40 +82,44 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -69,40 +82,44 @@ You need to setup at least one authentication method to enable two-factor authen
<div class="row mt-3"> <div class="row mt-3">
<div class="col-12 col-md-5"> <div class="col-12 col-md-5">
<h4>Authenticator Apps</h4> <h4>{{_("Authenticator Apps (TOTP)")}}</h4>
<p>Use an authenticator application on your mobile device as a second factor.</p> <p>
<p>The authenticator app generates a 6-digit one-time code each time you login. {{_("Use an authenticator application on your mobile device as a second factor.")}}
Compatible apps are freely available for most phones.</p> </p>
<p>
{{_("The authenticator app generates a 6-digit one-time code each time you login.
Compatible apps are freely available for most phones.")}}
</p>
</div> </div>
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
<form class="form mb-2" action="{{ url_for('mfa.setup_totp') }}"> <form class="form mb-2" action="{{ url_for('selfservice.setup_mfa_totp') }}" autocomplete="off">
<div class="row m-0"> <div class="row m-0">
<label class="sr-only" for="totp-name">Name</label> <label class="sr-only" for="totp-name">{{_("Name")}}</label>
<input type="text" name="name" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="totp-name" placeholder="Name" required {{ 'disabled' if mfa_init }}> <input type="text" name="name" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="totp-name" placeholder="{{_("Name")}}" required {{ 'disabled' if mfa_init }}>
<button type="submit" id="totp-submit" class="btn btn-primary mb-2 col" {{ 'disabled' if mfa_init }}>Setup new app</button> <button type="submit" id="totp-submit" class="btn btn-primary mb-2 col" {{ 'disabled' if mfa_init }}>{{_("Setup new app")}}</button>
</div> </div>
</form> </form>
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th scope="col">Name</th> <th scope="col">{{_("Name")}}</th>
<th scope="col">Registered On</th> <th scope="col">{{_("Registered On")}}</th>
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for method in totp_methods %} {% for method in request.user.mfa_totp_methods %}
<tr> <tr>
<td>{{ method.name }}</td> <td>{{ method.name }}</td>
<td>{{ method.created.strftime('%b %d, %Y') }}</td> <td>{{ method.created|dateformat }}</td>
<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_totp', id=method.id) }}">Delete</a></td> <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('selfservice.delete_mfa_totp', id=method.id) }}">{{_("Delete")}}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if not totp_methods %} {% if not request.user.mfa_totp_methods %}
<tr class="table-secondary"> <tr class="table-secondary">
<td colspan=3 class="text-center">No authenticator apps registered yet</td> <td colspan=3 class="text-center">{{_("No authenticator apps registered yet")}}</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
...@@ -114,24 +131,34 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -114,24 +131,34 @@ You need to setup at least one authentication method to enable two-factor authen
<div class="row"> <div class="row">
<div class="col-12 col-md-5"> <div class="col-12 col-md-5">
<h4>U2F and FIDO2 Devices</h4> <h4>{{_("U2F and FIDO2 Devices")}}</h4>
<p>Use an U2F or FIDO2 compatible hardware security key as a second factor.</p> <p>
<p>U2F and FIDO2 devices are not supported by all browsers and can be particularly difficult to use on mobile devices. {{_("Use an U2F or FIDO2 compatible hardware security key as a second factor.")}}
<strong>It is strongly recommended to also setup an authenticator app</strong> to be able to login on all browsers.</p> </p>
<p>
{{_("U2F and FIDO2 devices are not supported by all browsers and can be particularly difficult to use on mobile
devices. <strong>It is strongly recommended to also setup an authenticator app</strong> to be able to login on all
browsers.")}}
</p>
</div> </div>
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
{% if not webauthn_supported %}
<div class="alert alert-warning" role="alert">{{_("U2F/FIDO2 support not enabled")}}</div>
{% endif %}
<noscript> <noscript>
<div class="alert alert-warning" role="alert">Enable javascript in your browser to use U2F and FIDO2 devices!</div> <div class="alert alert-warning" role="alert">
{{_("Enable javascript in your browser to use U2F and FIDO2 devices!")}}
</div>
</noscript> </noscript>
<div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div> <div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
<form id="webauthn-form" class="form mb-2"> <form id="webauthn-form" autocomplete="off" class="form mb-2">
<div class="row m-0"> <div class="row m-0">
<label class="sr-only" for="webauthn-name">Name</label> <label class="sr-only" for="webauthn-name">{{_("Name")}}</label>
<input type="text" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="webauthn-name" placeholder="Name" required disabled> <input type="text" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 15em;" id="webauthn-name" placeholder="{{_("Name")}}" required disabled>
<button type="submit" id="webauthn-btn" class="btn btn-primary mb-2 col" disabled> <button type="submit" id="webauthn-btn" class="btn btn-primary mb-2 col" disabled>
<span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> <span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span id="webauthn-btn-text">Setup new device</span> <span id="webauthn-btn-text">{{_("Setup new device")}}</span>
</button> </button>
</div> </div>
</form> </form>
...@@ -139,22 +166,22 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -139,22 +166,22 @@ You need to setup at least one authentication method to enable two-factor authen
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th scope="col">Name</th> <th scope="col">{{_("Name")}}</th>
<th scope="col">Registered On</th> <th scope="col">{{_("Registered On")}}</th>
<th scope="col"></th> <th scope="col"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for method in webauthn_methods %} {% for method in request.user.mfa_webauthn_methods %}
<tr> <tr>
<td>{{ method.name }}</td> <td>{{ method.name }}</td>
<td>{{ method.created.strftime('%b %d, %Y') }}</td> <td>{{ method.created|dateformat }}</td>
<td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('mfa.delete_webauthn', id=method.id) }}">Delete</a></td> <td><a class="btn btn-sm btn-danger float-right" href="{{ url_for('selfservice.delete_mfa_webauthn', id=method.id) }}">{{_("Delete")}}</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
{% if not webauthn_methods %} {% if not request.user.mfa_webauthn_methods %}
<tr class="table-secondary"> <tr class="table-secondary">
<td colspan=3 class="text-center">No U2F/FIDO2 devices registered yet</td> <td colspan=3 class="text-center">{{_("No U2F/FIDO2 devices registered yet")}}</td>
</tr> </tr>
{% endif %} {% endif %}
</tbody> </tbody>
...@@ -162,27 +189,28 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -162,27 +189,28 @@ You need to setup at least one authentication method to enable two-factor authen
</div> </div>
</div> </div>
{% if webauthn_supported %}
<script src="{{ url_for('static', filename="cbor.js") }}"></script> <script src="{{ url_for('static', filename="cbor.js") }}"></script>
<script> <script>
$('#webauthn-form').on('submit', function(e) { $('#webauthn-form').on('submit', function(e) {
$('#webauthn-alert').addClass('d-none'); $('#webauthn-alert').addClass('d-none');
$('#webauthn-spinner').removeClass('d-none'); $('#webauthn-spinner').removeClass('d-none');
$('#webauthn-btn-text').text('Contacting server'); $('#webauthn-btn-text').text({{ _('Contacting server')|tojson }});
$('#webauthn-btn').prop('disabled', true); $('#webauthn-btn').prop('disabled', true);
fetch({{ url_for('mfa.setup_webauthn_begin')|tojson }}, { fetch({{ url_for('selfservice.setup_mfa_webauthn_begin')|tojson }}, {
method: 'POST', method: 'POST',
}).then(function(response) { }).then(function(response) {
if (response.ok) if (response.ok)
return response.arrayBuffer(); return response.arrayBuffer();
if (response.status == 403) if (response.status == 403)
throw new Error('You need to generate recovery codes first'); throw new Error({{ _('You need to generate recovery codes first')|tojson }});
throw new Error('Server error'); throw new Error({{ _('Server error')|tojson }});
}).then(CBOR.decode).then(function(options) { }).then(CBOR.decode).then(function(options) {
$('#webauthn-btn-text').text('Waiting for response from your device'); $('#webauthn-btn-text').text({{ _('Waiting for device')|tojson }});
return navigator.credentials.create(options); return navigator.credentials.create(options);
}).then(function(attestation) { }).then(function(attestation) {
return fetch({{ url_for('mfa.setup_webauthn_complete')|tojson }}, { return fetch({{ url_for('selfservice.setup_mfa_webauthn_complete')|tojson }}, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/cbor'}, headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({ body: CBOR.encode({
...@@ -194,34 +222,34 @@ $('#webauthn-form').on('submit', function(e) { ...@@ -194,34 +222,34 @@ $('#webauthn-form').on('submit', function(e) {
}).then(function(response) { }).then(function(response) {
if (response.ok) { if (response.ok) {
$('#webauthn-spinner').addClass('d-none'); $('#webauthn-spinner').addClass('d-none');
$('#webauthn-btn-text').text('Success'); $('#webauthn-btn-text').text({{ _('Success')|tojson }});
window.location = {{ url_for('mfa.setup')|tojson }}; window.location = {{ url_for('selfservice.setup_mfa')|tojson }};
} else { } else {
throw new Error('Response from authenticator rejected'); throw new Error({{ _('Invalid response from device')|tojson }});
} }
}, function(err) { }, function(err) {
console.log(err); console.log(err);
/* various webauthn errors */ /* various webauthn errors */
if (err.name == 'NotAllowedError') if (err.name == 'NotAllowedError')
$('#webauthn-alert').text('Registration timed out, was aborted or not allowed'); $('#webauthn-alert').text({{ _('Registration timed out, was aborted or not allowed')|tojson }});
else if (err.name == 'InvalidStateError') else if (err.name == 'InvalidStateError')
$('#webauthn-alert').text('You attempted to register a device that is already registered'); $('#webauthn-alert').text({{ _('Device already registered')|tojson }});
else if (err.name == 'AbortError') else if (err.name == 'AbortError')
$('#webauthn-alert').text('Registration was aborted'); $('#webauthn-alert').text({{ _('Registration was aborted')|tojson }});
else if (err.name == 'NotSupportedError') else if (err.name == 'NotSupportedError')
$('#webauthn-alert').text('U2F and FIDO2 devices are not supported by your browser'); $('#webauthn-alert').text({{ _('U2F and FIDO2 devices are not supported by your browser')|tojson }});
/* errors from fetch() */ /* errors from fetch() */
else if (err.name == 'TypeError') else if (err.name == 'TypeError')
$('#webauthn-alert').text('Could not connect to server'); $('#webauthn-alert').text({{ _('Could not connect to server')|tojson }});
/* our own errors */ /* our own errors */
else if (err.name == 'Error') else if (err.name == 'Error')
$('#webauthn-alert').text(err.message); $('#webauthn-alert').text(err.message);
/* fallback */ /* fallback */
else else
$('#webauthn-alert').text('Registration failed ('+err+')'); $('#webauthn-alert').text({{ _('Registration failed')|tojson }}+' ('+err+')');
$('#webauthn-alert').removeClass('d-none'); $('#webauthn-alert').removeClass('d-none');
$('#webauthn-spinner').addClass('d-none'); $('#webauthn-spinner').addClass('d-none');
$('#webauthn-btn-text').text('Retry registration'); $('#webauthn-btn-text').text({{ _('Retry registration')|tojson }});
$('#webauthn-btn').prop('disabled', false); $('#webauthn-btn').prop('disabled', false);
}); });
return false; return false;
...@@ -233,10 +261,11 @@ if (typeof(PublicKeyCredential) != "undefined") { ...@@ -233,10 +261,11 @@ if (typeof(PublicKeyCredential) != "undefined") {
$('#webauthn-name').prop('disabled', false); $('#webauthn-name').prop('disabled', false);
{% endif %} {% endif %}
} else { } else {
$('#webauthn-alert').text('U2F and FIDO2 devices are not supported by your browser'); $('#webauthn-alert').text({{ _('U2F and FIDO2 devices are not supported by your browser')|tojson }});
$('#webauthn-alert').removeClass('d-none'); $('#webauthn-alert').removeClass('d-none');
} }
</script> </script>
{% endif %}
{% endblock %} {% endblock %}
...@@ -2,24 +2,32 @@ ...@@ -2,24 +2,32 @@
{% block body %} {% block body %}
<h1 class="d-none d-print-block">Recovery Codes</h1> <h1 class="d-none d-print-block">{{_("Recovery Codes")}}</h1>
<p>Recovery codes allow you to login when you lose access to your authenticator app or U2F/FIDO device. Each code can only be used once.</p> <p>
{{_("Recovery codes allow you to login when you lose access to your authenticator app or U2F/FIDO device. Each code can
only be used once.")}}
</p>
<div class="text-monospace"> <div class="text-monospace">
<ul> <ul>
{% for method in methods %} {% for method in methods %}
<li>{{ method.code }}</li> <li>{{ method.code_value }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
<p>These are your new recovery codes. Make sure to store them in a safe place or you risk losing access to your account. All previous recovery codes are now invalid.</p> <p>
{{_("These are your new recovery codes. Make sure to store them in a safe place or you risk losing access to your
account. All previous recovery codes are now invalid.")}}
</p>
<div class="btn-toolbar"> <div class="btn-toolbar">
<a class="ml-auto mb-2 btn btn-primary d-print-none" href="{{ url_for('mfa.setup') }}">Continue</a> <a class="ml-auto mb-2 btn btn-primary d-print-none" href="{{ url_for('selfservice.setup_mfa') }}">{{_("Continue")}}</a>
<a class="ml-2 mb-2 btn btn-light d-print-none" href="{{ methods|map(attribute='code')|join('\n')|datauri }}" download="uffd-recovery-codes">Download codes</a> <a class="ml-2 mb-2 btn btn-light d-print-none" href="{{ methods|map(attribute='code_value')|join('\n')|datauri }}" download="uffd-recovery-codes">
<button class="ml-2 mb-2 btn btn-light d-print-none" type="button" onClick="window.print()">Print codes</button> {{_("Download codes")}}
</a>
<button class="ml-2 mb-2 btn btn-light d-print-none" type="button" onClick="window.print()">{{_("Print codes")}}</button>
</div> </div>
{% endblock %} {% endblock %}
...@@ -2,7 +2,10 @@ ...@@ -2,7 +2,10 @@
{% block body %} {% block body %}
<p>Install an authenticator application on your mobile device like FreeOTP or Google Authenticator and scan this QR code. On Apple devices you can use an app called "Authenticator".</p> <p>
{{_("Install an authenticator application on your mobile device like FreeOTP or Google Authenticator and scan this QR
code. On Apple devices you can use an app called \"Authenticator\".")}}
</p>
<div class="row"> <div class="row">
<div class="mx-auto col-9 col-md-4 mb-3"> <div class="mx-auto col-9 col-md-4 mb-3">
...@@ -11,24 +14,28 @@ ...@@ -11,24 +14,28 @@
</a> </a>
</div> </div>
<div class="col-12 col-md-8"> <div class="col-12 col-md-8">
<p>If you are on your mobile device and cannot scan the code, you can click on it to open it with your authenticator app. If that does not work, enter the following details manually into your authenticator app:</p>
<p> <p>
Issuer: {{ method.issuer }}<br> {{_("If you are on your mobile device and cannot scan the code, you can click on it to open it with your
Account: {{ method.accountname }}<br> authenticator app. If that does not work, enter the following details manually into your authenticator
Secret: {{ method.key }}<br> app:")}}
Type: TOTP (time-based)<br> </p>
Digits: 6<br> <p>
Hash algorithm: SHA1<br> {{_("Issuer")}}: {{ method.issuer }}<br>
Interval/period: 30 seconds {{_("Account")}}: {{ method.accountname }}<br>
{{_("Secret")}}: {{ method.key }}<br>
{{_("Type")}}: TOTP (time-based)<br>
{{_("Digits")}}: 6<br>
{{_("Hash algorithm")}}: SHA1<br>
{{_("Interval/period")}}: 30 {{_("seconds")}}
</p> </p>
</div> </div>
</div> </div>
<form action="{{ url_for('mfa.setup_totp_finish', name=name) }}" method="POST" class="form"> <form action="{{ url_for('selfservice.setup_mfa_totp_finish', name=name) }}" method="POST" autocomplete="off" class="form">
<div class="row m-0"> <div class="row m-0">
<input type="text" name="code" class="form-control mb-2 mr-2 col-auto col-md" id="code" placeholder="Code" required autofocus> <input type="text" name="code" class="form-control mb-2 mr-2 col-auto col-md" id="code" placeholder="{{_('Code')}}" required autofocus>
<button type="submit" class="btn btn-primary mb-2 col col-md-auto">Verify and complete setup</button> <button type="submit" class="btn btn-primary mb-2 col col-md-auto">{{_("Verify and complete setup")}}</button>
</div> </div>
</form> </form>
......
{% extends 'base.html' %}
{% block body %}
<div class="row">
<form action="{{ url_for('service.api_submit', service_id=service.id, id=client.id) }}" method="POST" autocomplete="off" class="form col-12 px-0">
<div class="form-group col">
<p class="text-right">
<a href="{{ url_for('service.show', id=service.id) }}" class="btn btn-secondary">{{ _('Cancel') }}</a>
{% if client.id %}
<a class="btn btn-danger" href="{{ url_for('service.api_delete', service_id=service.id, id=client.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});'>
<i class="fa fa-trash" aria-hidden="true"></i> {{_('Delete')}}
</a>
{% endif %}
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{ _('Save') }}</button>
</p>
</div>
<div class="form-group col">
<label for="client-auth-username">{{ _('Authentication Username') }}</label>
<input type="text" class="form-control" id="client-auth-username" name="auth_username" value="{{ client.auth_username or '' }}" required>
</div>
<div class="form-group col">
<label for="client-auth-password">{{ _('Authentication Password') }}</label>
{% if client.id %}
<input type="password" autocomplete="new-password" class="form-control" id="client-auth-password" name="auth_password" placeholder="●●●●●●●●">
{% else %}
<input type="password" autocomplete="new-password" class="form-control" id="client-auth-password" name="auth_password" required>
{% endif %}
</div>
<div class="form-group col">
<h6>{{ _('Permissions') }}</h6>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="client-perm-users" name="perm_users" value="1" aria-label="enabled" {{ 'checked' if client.perm_users }}>
<label class="form-check-label" for="client-perm-users"><b>users</b>: {{_('Access user and group data')}}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="client-perm-checkpassword" name="perm_checkpassword" value="1" aria-label="enabled" {{ 'checked' if client.perm_checkpassword }}>
<label class="form-check-label" for="client-perm-checkpassword"><b>checkpassword</b>: {{_('Verify user passwords')}}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="client-perm-mail-aliases" name="perm_mail_aliases" value="1" aria-label="enabled" {{ 'checked' if client.perm_mail_aliases }}>
<label class="form-check-label" for="client-perm-mail-aliases"><b>mail_aliases</b>: {{_('Access mail aliases')}}</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="client-perm-remailer" name="perm_remailer" value="1" aria-label="enabled" {{ 'checked' if client.perm_remailer }}>
<label class="form-check-label" for="client-perm-remailer"><b>remailer</b>: {{_('Resolve remailer addresses')}}</label>
{% if not remailer.configured %}
<i class="fas fa-exclamation-triangle text-warning" data-toggle="tooltip" data-placement="top" title="{{ _('This option has no effect: Remailer config options are unset') }}"></i>
{% endif %}
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="client-perm-metrics" name="perm_metrics" value="1" aria-label="enabled" {{ 'checked' if client.perm_metrics }}>
<label class="form-check-label" for="client-perm-metrics"><b>metrics</b>: {{_('Access uffd metrics')}}</label>
</div>
</div>
</form>
</div>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<div class="row">
<div class="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for('service.show') }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
</p>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">{{ _('Name') }}</th>
</tr>
</thead>
<tbody>
{% for service in services|sort(attribute="name") %}
<tr>
<td>
<a href="{{ url_for("service.show", id=service.id) }}">
{{ service.name }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<div class="row">
<form action="{{ url_for('service.oauth2_submit', service_id=service.id, db_id=client.db_id) }}" method="POST" autocomplete="off" class="form col-12 px-0">
<div class="form-group col">
<p class="text-right">
<a href="{{ url_for('service.show', id=service.id) }}" class="btn btn-secondary">{{ _('Cancel') }}</a>
{% if client.db_id %}
<a class="btn btn-danger" href="{{ url_for('service.oauth2_delete', service_id=service.id, db_id=client.db_id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});'>
<i class="fa fa-trash" aria-hidden="true"></i> {{_('Delete')}}
</a>
{% endif %}
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{ _('Save') }}</button>
</p>
</div>
<div class="form-group col">
<label for="client-client-id">{{ _('Client ID') }}</label>
<input type="text" class="form-control" id="client-client-id" name="client_id" value="{{ client.client_id or '' }}" required>
</div>
<div class="form-group col">
<label for="client-client-secret">{{ _('Client Secret') }}</label>
{% if client.db_id %}
<input type="password" autocomplete="new-password" class="form-control" id="client-client-secret" name="client_secret" placeholder="●●●●●●●●">
{% else %}
<input type="password" autocomplete="new-password" class="form-control" id="client-client-secret" name="client_secret" required>
{% endif %}
</div>
<div class="form-group col">
<label for="client-redirect-uris">{{ _('Redirect URIs') }}</label>
<textarea rows="3" class="form-control" id="client-redirect-uris" name="redirect_uris">{{ client.redirect_uris|join('\n') }}</textarea>
<small class="form-text text-muted">
{{ _('One URI per line') }}
</small>
</div>
<div class="form-group col">
<label for="client-logout-uris">{{ _('Logout URIs') }}</label>
<textarea rows="3" class="form-control" id="client-logout-uris" name="logout_uris" placeholder="GET https://example.com/logout">
{%- for logout_uri in client.logout_uris %}
{{ logout_uri.method }} {{ logout_uri.uri }}{{ '\n' if not loop.last }}
{%- endfor %}
</textarea>
<small class="form-text text-muted">
{{ _('One URI per line, prefixed with space-separated method (GET/POST)') }}
</small>
</div>
</form>
</div>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
{% set iconstyle = 'style="width: 1.8em;"'|safe %}
{% if not request.user %}
<div class="alert alert-warning" role="alert">
<div class="row">
<div class="col-12 col-md-9 col-lg-10 col-xl-10">
{{ _("Some services may not be publicly listed! Log in to see all services you have access to.") }}
</div>
<div class="col-12 col-md-3 col-lg-2 col-xl-2 text-center text-md-right text-lg-right text-xl-right">
<a class="btn btn-primary" href="{{ url_for("session.login", ref=request.full_path) }}">
<i class="fa fa-sign-in-alt" aria-hidden="true"></i> {{ _("Login") }}
</a>
</div>
</div>
</div>
{% endif %}
{% if banner %}
<div class="card">
<div class="card-body">
{{ banner|safe }}
</div>
</div>
{% endif %}
{% macro service_card(service) %}
<div class="col mb-4">
<div class="card h-100 {{ 'text-muted' if not service.has_access }}">
<div class="card-body">
{% if service.logo_url %}
{% if service.url and service.has_access %}<a href="{{ service.url }}" class="text-reset">{% endif %}
<img alt="{{ _("Logo for %(service_title)s", service_title=service.title) }}" src="{{ service.logo_url }}" style="width: 100%; height: 10em; object-fit: contain; {{ 'filter: grayscale(100%);' if not service.has_access }}">
{% if service.url and service.has_access %}</a>{% endif %}
{% endif %}
<h5 class="card-title">
{% if service.url and service.has_access %}
<a href="{{ service.url }}" class="text-reset">{{ service.title }}</a>
{% else %}
{{ service.title }}
{% endif %}
</h5>
{% if service.subtitle %}
<h6 class="card-subtitle mb-2 text-muted">{{ service.subtitle }}</h6>
{% endif %}
{% if service.description %}
<p class="card-text">{{ service.description }}</p>
{% endif %}
</div>
<div class="list-group list-group-flush">
{% if not service.has_access %}
<div class="list-group-item"><i class="fas fa-shield-alt" {{ iconstyle }}></i> {{_("No access")}}</div>
{% elif service.permission %}
<div class="list-group-item"><i class="fas fa-shield-alt" {{ iconstyle }}></i> {{ service.permission }}</div>
{% endif %}
{% for group in service.groups %}
<div class="list-group-item"><i class="fas fa-users" {{ iconstyle }}></i> {{ group.name }}</div>
{% endfor %}
{% for info in service.infos %}
<a href="#" class="list-group-item list-group-item-action" data-toggle="modal" data-target="#info-modal-{{ info.id }}"><i class="fas fa-info-circle" {{ iconstyle }}></i> {{ info.button_text }}</a>
{% endfor %}
{% for link in service.links %}
<a href="{{ link.url }}" class="list-group-item list-group-item-action"><i class="fas fa-external-link-alt" {{ iconstyle }}></i> {{ link.title }}</a>
{% endfor %}
</div>
</div>
</div>
{% endmacro %}
{% if request.user and request.user.is_in_group(config['ACL_ADMIN_GROUP']) %}
<div class="text-right mt-2">
<a href="{{ url_for('service.index') }}" class="btn btn-primary">{{ _('Manage OAuth2 and API clients') }}</a>
</div>
{% endif %}
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 mt-2">
{% for service in services if service.has_access %}
{{ service_card(service) }}
{% endfor %}
{% for service in services if not service.has_access %}
{{ service_card(service) }}
{% endfor %}
</div>
{% for service in services %}
{% for info in service.infos %}
<div class="modal" tabindex="-1" id="info-modal-{{ info.id }}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{ info.title }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="{{_("Close")}}">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ info.html|safe }}
</div>
</div>
</div>
</div>
{% endfor %}
{% endfor %}
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<div class="row">
<form action="{{ url_for('service.edit_submit', id=service.id) }}" method="POST" autocomplete="off" class="form col-12 px-0">
<div class="form-group col">
<p class="text-right">
<a href="{{ url_for('service.index') }}" class="btn btn-secondary">{{ _('Cancel') }}</a>
{% if service.id %}
<a class="btn btn-danger" href="{{ url_for('service.delete', id=service.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});'>
<i class="fa fa-trash" aria-hidden="true"></i> {{_('Delete')}}
</a>
{% endif %}
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{ _('Save') }}</button>
</p>
</div>
<div class="form-group col">
<label for="service-name">{{ _('Name') }}</label>
<input type="text" class="form-control" id="service-name" name="name" value="{{ service.name or '' }}" required>
</div>
<div class="form-group col">
<label for="access-group">{{ _('Access Restriction') }}</label>
<select class="form-control" id="access-group" name="access-group">
<option value="" class="text-muted">{{ _('No user has access') }}</option>
<option value="all" class="text-muted" {{ 'selected' if not service.limit_access }}>{{ _('All users have access (legacy)') }}</option>
{% for group in all_groups %}
<option value="{{ group.id }}" {{ 'selected' if group == service.access_group and service.limit_access }}>{{ _('Members of group "%(group_name)s" have access', group_name=group.name) }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="hide-deactivated-users" name="hide_deactivated_users" value="1" aria-label="enabled" {{ 'checked' if service.hide_deactivated_users }}>
<label class="form-check-label" for="hide-deactivated-users">{{ _('Hide deactivated users from service') }}</label>
</div>
</div>
<div class="form-group col">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="service-enable-email-preferences" name="enable_email_preferences" value="1" aria-label="enabled" {{ 'checked' if service.enable_email_preferences }}>
<label class="form-check-label" for="service-enable-email-preferences">{{ _('Allow users with access to select a different e-mail address for this service') }}</label>
<small class="form-text text-muted">
{{ _('If disabled, the service always uses the primary e-mail address.') }}
</small>
</div>
</div>
<div class="form-group col">
<label for="remailer-mode">
{{ _('Hide e-mail addresses with remailer') }}
{% if not remailer.configured %}
<i class="fas fa-exclamation-triangle text-warning" data-toggle="tooltip" data-placement="top" title="{{ _('This option has no effect: Remailer config options are unset') }}"></i>
{% endif %}
</label>
<select class="form-control" id="remailer-mode" name="remailer-mode">
<option value="{{ RemailerMode.DISABLED.name }}" {{ 'selected' if service.remailer_mode == RemailerMode.DISABLED }}>
{{ _('Remailer disabled') }}
</option>
<option value="{{ RemailerMode.ENABLED_V2.name }}" {{ 'selected' if service.remailer_mode == RemailerMode.ENABLED_V2 }}>
{{ _('Remailer enabled') }}
</option>
<option value="{{ RemailerMode.ENABLED_V1.name }}" {{ 'selected' if service.remailer_mode == RemailerMode.ENABLED_V1 }}>
{{ _('Remailer enabled (deprecated, case-sensitive format)') }}
</option>
</select>
<small class="form-text text-muted">
{{ _('Some services notify users about changes to their e-mail address. Modifying this setting immediatly affects the e-mail addresses of all users and can cause masses of notification e-mails.') }}
</small>
</div>
<div class="form-group col">
<p class="mb-2">
{{ _('Overwrite remailer setting for specific users') }}
</p>
<div class="input-group" id="remailer-mode-overwrite">
<input class="form-control" name="remailer-overwrite-users" placeholder="{{ _('Login names') }}" value="{{ remailer_overwrites|map(attribute='user')|map(attribute='loginname')|sort|join(', ') }}">
<select class="form-control" name="remailer-overwrite-mode">
{% set remailer_overwrite_mode = remailer_overwrites|map(attribute='remailer_overwrite_mode')|first or RemailerMode.ENABLED_V2 %}
<option value="{{ RemailerMode.DISABLED.name }}" {{ 'selected' if remailer_overwrite_mode == RemailerMode.DISABLED }}>
{{ _('Remailer disabled') }}
</option>
<option value="{{ RemailerMode.ENABLED_V2.name }}" {{ 'selected' if remailer_overwrite_mode == RemailerMode.ENABLED_V2 }}>
{{ _('Remailer enabled') }}
</option>
<option value="{{ RemailerMode.ENABLED_V1.name }}" {{ 'selected' if remailer_overwrite_mode == RemailerMode.ENABLED_V1 }}>
{{ _('Remailer enabled (deprecated, case-sensitive format)') }}
</option>
</select>
</div>
<small class="form-text text-muted">
{{ _('Useful for testing remailer before enabling it for all users. Specify users as a comma-seperated list of login names.') }}
</small>
</div>
</form>
{% if service.id %}
<div class="col-12">
<hr>
<h5>OAuth2 Clients</h5>
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for('service.oauth2_show', service_id=service.id) }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
</p>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">{{ _('Client ID') }}</th>
</tr>
</thead>
<tbody>
{% for client in service.oauth2_clients|sort(attribute='client_id') %}
<tr>
<td>
<a href="{{ url_for("service.oauth2_show", service_id=service.id, db_id=client.db_id) }}">
{{ client.client_id }}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-12">
<hr>
<h5>API Clients</h5>
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for('service.api_show', service_id=service.id) }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
</p>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">{{ _('Name') }}</th>
<th scope="col">{{ _('Permissions') }}</th>
</tr>
</thead>
<tbody>
{% for client in service.api_clients|sort(attribute='auth_username') %}
<tr>
<td>
<a href="{{ url_for("service.api_show", service_id=service.id, id=client.id) }}">
{{ client.auth_username }}
</a>
</td>
<td>
{% for perm in ['users', 'checkpassword', 'mail_aliases', 'remailer'] if client.has_permission(perm) %}
{{ perm }}{{ ',' if not loop.last }}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
<script>
$(function () {
$('[data-toggle="tooltip"]').tooltip()
})
</script>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
{% if not initiation %}
<form action="{{ url_for("session.deviceauth") }}" autocomplete="off">
{% elif not confirmation %}
<form action="{{ url_for("session.deviceauth_submit") }}" method="POST" autocomplete="off">
{% else %}
<form action="{{ url_for("session.deviceauth_finish") }}" method="POST" autocomplete="off">
{% endif %}
<div class="col-12">
<h2 class="text-center">{{_('Authorize Device Login')}}</h2>
</div>
<div class="form-group col-12">
<p>{{_('Log into a service on another device without entering your password.')}}</p>
</div>
<div class="form-group col-12">
<label for="initiation-code">{{_('Initiation Code')}}</label>
{% if not initiation %}
<input type="text" class="form-control" id="initiation-code" name="initiation-code" value="{{ initiation_code or '' }}" required="required" tabindex = "1" autofocus>
{% else %}
<input type="text" class="form-control" id="initiation-code" name="initiation-code" value="{{ initiation.code }}" readonly>
{% endif %}
</div>
{% if confirmation %}
<div class="form-group col-12">
<label for="confirmation-code">{{_('Confirmation Code')}}</label>
<input type="text" class="form-control" id="confirmation-code" name="confirmation-code" value="{{ confirmation.code }}" readonly>
</div>
{% endif %}
{% if not initiation %}
<div class="form-group col-12">
<p>{{_('Start logging into a service on the other device and chose "Device Login" on the login page. Enter the displayed initiation code in the box above.')}}</p>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "2">{{_('Continue')}}</button>
</div>
<div class="form-group col-12">
<a href="{{ url_for('index') }}" class="btn btn-secondary btn-block" tabindex="0">{{_('Cancel')}}</a>
</div>
{% elif not confirmation %}
<div class="form-group col-12">
<p>{{_('Authorize the login for service <b>%(service_name)s</b>?', service_name=initiation.description|e)|safe}}</p>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "2">{{_('Authorize Login')}}</button>
</div>
<div class="form-group col-12">
<a href="{{ url_for('index') }}" class="btn btn-secondary btn-block" tabindex="0">{{_('Cancel')}}</a>
</div>
{% else %}
<div class="form-group col-12">
<p>{{_('Enter the confirmation code on the other device and complete the login. Click <em>Finish</em> afterwards.')|safe}}</p>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "2">{{_('Finish')}}</button>
</div>
{% endif %}
</form>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for("session.devicelogin_submit", ref=ref) }}" method="POST" autocomplete="off">
<div class="col-12">
<h2 class="text-center">{{_('Device Login')}}</h2>
</div>
<div class="form-group col-12">
<p>{{_('Use a login session on another device (e.g. your laptop) to log into a service without entering your password.')}}</p>
</div>
{% if initiation %}
<div class="form-group col-12">
<label for="initiation-code">{{_('Initiation Code')}}</label>
<input type="text" class="form-control" id="initiation-code" name="initiation-code" value="{{ initiation.code }}" readonly>
</div>
<input type="hidden" class="form-control" id="initiation-secret" name="initiation-secret" value="{{ initiation.secret }}">
<div class="form-group col-12">
<label for="confirmation-code">{{_('Confirmation Code')}}</label>
<input type="text" class="form-control" id="confirmation-code" name="confirmation-code" required="required" tabindex = "1" autofocus>
</div>
<div class="form-group col-12">
<p>{{_('Open <code><a href="%(deviceauth_url)s">%(deviceauth_url)s</a></code> on the other device and enter the initiation code there. Then enter the confirmation code in the box above.', deviceauth_url=url_for('session.deviceauth', _external=True)|e)|safe}}</p>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex = "3">{{_('Continue')}}</button>
</div>
{% endif %}
<div class="form-group col-12">
<a href="{{ url_for('session.login', ref=ref, devicelogin=True) }}" class="btn btn-secondary btn-block" tabindex="0">{{_('Cancel')}}</a>
</div>
</form>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for("session.login", ref=ref) }}" method="POST">
<div class="col-12">
<h2 class="text-center">{{_("Login")}}</h2>
</div>
{% if config['LOGIN_BANNER'] %}
<p>{{ config['LOGIN_BANNER'] }}</p>
{% endif %}
<div class="form-group col-12">
<label for="user-loginname">{{_("Login Name")}}</label>
<input type="text" autocomplete="username" class="form-control" id="user-loginname" name="loginname" required="required" tabindex="1" autofocus>
</div>
<div class="form-group col-12">
<label for="user-password1">{{_("Password")}}</label>
<input type="password" autocomplete="current-password" class="form-control" id="user-password1" name="password" required="required" tabindex="2">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex="3">{{_("Login")}}</button>
</div>
{% if request.values.get('devicelogin') %}
<div class="text-center text-muted mb-3">{{_("- or -")}}</div>
<div class="form-group col-12">
<a href="{{ url_for('session.devicelogin_start', ref=ref) }}" class="btn btn-primary btn-block" tabindex="0">{{_("Login with another device")}}</a>
</div>
{% endif %}
<div class="clearfix col-12">
{% if config['SELF_SIGNUP'] %}
<a href="{{ url_for("signup.signup_start") }}" class="float-left">{{_("Register")}}</a>
{% endif %}
<a href="{{ url_for("selfservice.forgot_password") }}" class="float-right">{{_("Forgot Password?")}}</a>
</div>
</form>
{% endblock %}
{% extends 'base.html' %} {% extends 'base_narrow.html' %}
{% block body %} {% block body %}
<form action="{{ url_for("session.mfa_auth_finish", ref=ref) }}" method="POST" autocomplete="off">
<form action="{{ url_for("mfa.auth_finish", ref=ref) }}" method="POST"> <div class="col-12 mb-3">
<div class="row mt-2 justify-content-center"> <h2 class="text-center">{{_("Two-Factor Authentication")}}</h2>
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> </div>
<div class="text-center"> {% if request.user_pre_mfa.mfa_webauthn_methods %}
<img src="{{ url_for("static", filename="chaosknoten.png") }}" class="col-lg-8 col-md-12" > <noscript>
</div>
<div class="col-12 mb-3">
<h2 class="text-center">Two-Factor Authentication</h2>
</div>
{% if webauthn_methods %}
<noscript>
<div class="form-group col-12">
<div id="webauthn-nojs" class="alert alert-warning" role="alert">Enable javascript for authentication with U2F/FIDO2 devices</div>
</div>
</noscript>
<div id="webauthn-unsupported" class="form-group col-12 d-none">
<div class="alert alert-warning" role="alert">Authentication with U2F/FIDO2 devices is not supported by your browser</div>
</div>
<div class="form-group col-12 d-none webauthn-group">
<div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
<button type="button" id="webauthn-btn" class="btn btn-primary btn-block">
<span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span id="webauthn-btn-text">Authenticate with U2F/FIDO2 device</span>
</button>
</div>
<div class="text-center text-muted d-none webauthn-group mb-3">- or -</div>
{% endif %}
<div class="form-group col-12 mb-2">
<input type="text" class="form-control" id="mfa-code" name="code" required="required" placeholder="Code from your authenticator app or recovery code" autocomplete="off" autofocus>
</div>
<div class="form-group col-12"> <div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">Verify</button> <div id="webauthn-nojs" class="alert alert-warning" role="alert">{{_("Enable javascript for authentication with U2F/FIDO2 devices")}}</div>
</div> </div>
</noscript>
<div id="webauthn-unsupported" class="form-group col-12 d-none">
<div class="alert alert-warning" role="alert">{{_("Authentication with U2F/FIDO2 devices is not supported by your browser")}}</div>
</div>
<div class="form-group col-12 d-none webauthn-group">
<div id="webauthn-alert" class="alert alert-warning d-none" role="alert"></div>
<button type="button" id="webauthn-btn" class="btn btn-primary btn-block">
<span id="webauthn-spinner" class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<span id="webauthn-btn-text">{{_("Authenticate with U2F/FIDO2 device")}}</span>
</button>
</div>
<div class="text-center text-muted d-none webauthn-group mb-3">- {{_("or")}} -</div>
{% endif %}
<div class="form-group col-12 mb-2">
<input type="text" class="form-control" id="mfa-code" name="code" required="required" placeholder="{{_("Code from your authenticator app or recovery code")}}" autofocus>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_("Verify")}}</button>
</div>
<div class="form-group col-12">
<a href="{{ url_for("session.logout") }}" class="btn btn-secondary btn-block">{{_("Cancel")}}</a>
</div> </div>
</div>
</form> </form>
{% if webauthn_methods %} {% if webauthn_supported and request.user_pre_mfa.mfa_webauthn_methods %}
<script src="{{ url_for('static', filename="cbor.js") }}"></script> <script src="{{ url_for('static', filename="cbor.js") }}"></script>
<script> <script>
function begin_webauthn() { function begin_webauthn() {
$('#webauthn-alert').addClass('d-none'); $('#webauthn-alert').addClass('d-none');
$('#webauthn-spinner').removeClass('d-none'); $('#webauthn-spinner').removeClass('d-none');
$('#webauthn-btn-text').text('Fetching credential data'); $('#webauthn-btn-text').text({{ _('Contacting server')|tojson }});
$('#webauthn-btn').prop('disabled', true); $('#webauthn-btn').prop('disabled', true);
fetch({{ url_for('mfa.auth_webauthn_begin')|tojson }}, { fetch({{ url_for('session.mfa_auth_webauthn_begin')|tojson }}, {
method: 'POST', method: 'POST',
}).then(function(response) { }).then(function(response) {
if(response.ok) return response.arrayBuffer(); if (response.ok) {
throw new Error('You have not registered any U2F/FIDO2 devices for your account'); return response.arrayBuffer();
} else if (response.status == 403) {
window.location = {{ request.url|tojson }}; /* reload */
throw new Error({{ _('Session timed out')|tojson }});
} else if (response.status == 404) {
throw new Error({{ _('You have not registered any U2F/FIDO2 devices for your account')|tojson }});
} else {
throw new Error({{ _('Server error')|tojson }});
}
}).then(CBOR.decode).then(function(options) { }).then(CBOR.decode).then(function(options) {
$('#webauthn-btn-text').text('Waiting for response from your device'); $('#webauthn-btn-text').text({{ _('Waiting for device')|tojson }});
return navigator.credentials.get(options); return navigator.credentials.get(options);
}).then(function(assertion) { }).then(function(assertion) {
$('#webauthn-btn-text').text('Verifing response'); $('#webauthn-btn-text').text({{ _('Verifing response')|tojson }});
return fetch({{ url_for('mfa.auth_webauthn_complete')|tojson }}, { return fetch({{ url_for('session.mfa_auth_webauthn_complete')|tojson }}, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/cbor'}, headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({ body: CBOR.encode({
...@@ -70,34 +73,37 @@ function begin_webauthn() { ...@@ -70,34 +73,37 @@ function begin_webauthn() {
}).then(function(response) { }).then(function(response) {
if (response.ok) { if (response.ok) {
$('#webauthn-spinner').addClass('d-none'); $('#webauthn-spinner').addClass('d-none');
$('#webauthn-btn-text').text('Success, redirecting'); $('#webauthn-btn-text').text({{ _('Success, redirecting')|tojson }});
window.location = {{ (ref or url_for('index'))|tojson }}; window.location = {{ (ref or url_for('index'))|tojson }};
} else if (response.status == 403) {
window.location = {{ request.url|tojson }}; /* reload */
throw new Error({{ _('Session timed out')|tojson }});
} else { } else {
throw new Error('Response from authenticator rejected'); throw new Error({{ _('Invalid response from device')|tojson }});
} }
}, function(err) { }).catch(function(err) {
console.log(err); console.log(err);
/* various webauthn errors */ /* various webauthn errors */
if (err.name == 'NotAllowedError') if (err.name == 'NotAllowedError')
$('#webauthn-alert').text('Authentication timed out, was aborted or not allowed'); $('#webauthn-alert').text({{ _('Authentication timed out, was aborted or not allowed')|tojson }});
else if (err.name == 'InvalidStateError') else if (err.name == 'InvalidStateError')
$('#webauthn-alert').text('Device is not registered for your account'); $('#webauthn-alert').text({{ _('Device is not registered for your account')|tojson }});
else if (err.name == 'AbortError') else if (err.name == 'AbortError')
$('#webauthn-alert').text('Authentication was aborted'); $('#webauthn-alert').text({{ _('Authentication was aborted')|tojson }});
else if (err.name == 'NotSupportedError') else if (err.name == 'NotSupportedError')
$('#webauthn-alert').text('U2F and FIDO2 devices are not supported by your browser'); $('#webauthn-alert').text({{ _('U2F and FIDO2 devices are not supported by your browser')|tojson }});
/* errors from fetch() */ /* errors from fetch() */
else if (err.name == 'TypeError') else if (err.name == 'TypeError')
$('#webauthn-alert').text('Could not connect to server'); $('#webauthn-alert').text({{ _('Could not connect to server')|tojson }});
/* our own errors */ /* our own errors */
else if (err.name == 'Error') else if (err.name == 'Error')
$('#webauthn-alert').text(err.message); $('#webauthn-alert').text(err.message);
/* fallback */ /* fallback */
else else
$('#webauthn-alert').text('Authentication failed ('+err+')'); $('#webauthn-alert').text({{ _('Authentication failed ')|tojson }}+'('+err+')');
$('#webauthn-alert').removeClass('d-none'); $('#webauthn-alert').removeClass('d-none');
$('#webauthn-spinner').addClass('d-none'); $('#webauthn-spinner').addClass('d-none');
$('#webauthn-btn-text').text('Try FIDO token again'); $('#webauthn-btn-text').text({{ _('Retry authenticate with U2F/FIDO2 device')|tojson }});
$('#webauthn-btn').prop('disabled', false); $('#webauthn-btn').prop('disabled', false);
}); });
} }
......
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for(".signup_confirm_submit", signup_id=signup.id, token=signup.token) }}" method="POST">
<div class="col-12">
<h2 class="text-center">{{_('Complete Registration')}}</h2>
</div>
<div class="form-group col-12">
<label for="user-password1">{{_('Please enter your password to complete the account registration')}}</label>
<input type="password" autocomplete="current-password" class="form-control" id="user-password1" name="password" required="required">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_('Complete Account Registration')}}</button>
</div>
</form>
{% endblock %}
Hi {{ signup.displayname }},
an account was created on the {{ config.ORGANISATION_NAME }} infrastructure with this mail address.
Please visit the following url to complete the account registration:
{{ url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token, _external=True) }}
**The link is valid for 48h**
{% if config.WELCOME_TEXT %}
{{ config.WELCOME_TEXT }}
{% endif -%}
If you have not requested an account on the {{ config.ORGANISATION_NAME }} infrastructure, you can ignore this mail.
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for('.signup_submit') }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '') ">
<div class="col-12">
<h2 class="text-center">{{_('Account Registration')}}</h2>
</div>
<div class="form-group col-12">
<label for="user-loginname">{{_('Login Name')}}</label>
<div class="js-only-input-group">
<input type="text" autocomplete="username" class="form-control" id="user-loginname" name="loginname" aria-describedby="loginname-feedback" value="{{ request.form.loginname }}" minlength=1 maxlength=32 pattern="[a-z0-9_-]*" required>
<div class="js-only-input-group-append d-none">
<button class="btn btn-outline-secondary rounded-right" type="button" id="check-loginname">{{_('Check')}}</button>
</div>
<div id="loginname-feedback" class="invalid-feedback"></div>
</div>
<small class="form-text text-muted">
{{_('At least one and at most 32 lower-case characters, digits, dashes ("-") or underscores ("_"). <b>Cannot be changed later!</b>')|safe}}
</small>
</div>
<div class="form-group col-12">
<label for="user-displayname">{{_('Display Name')}}</label>
<input type="text" autocomplete="nickname" class="form-control" id="user-displayname" name="displayname" value="{{ request.form.displayname }}" minlength=1 maxlength=128 required>
<small class="form-text text-muted">
{{_('At least one and at most 128 characters, no other special requirements.')}}
</small>
</div>
<div class="form-group col-12">
<label for="user-mail">{{_('E-Mail Address')}}</label>
<input type="email" autocomplete="email" class="form-control" id="user-mail" name="mail" value="{{ request.form.mail }}" required>
<small class="form-text text-muted">
{{_('We will send a confirmation mail to this address that you need to complete the registration.')}}
</small>
</div>
<div class="form-group col-12">
<label for="user-password1">{{_('Password')}}</label>
<input type="password" autocomplete="new-password" class="form-control" id="user-password1" name="password1" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}" required>
<small class="form-text text-muted">
{{ User.PASSWORD_DESCRIPTION|safe }}
</small>
</div>
<div class="form-group col-12">
<label for="user-password2">{{_('Repeat Password')}}</label>
<input type="password" autocomplete="new-password" class="form-control" id="user-password2" name="password2" required>
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_('Create Account')}}</button>
</div>
</form>
<script>
$(".js-only-input-group").addClass("input-group");
$(".js-only-input-group-append").removeClass("d-none");
$(".js-only-input-group-append").addClass("input-group-append");
let checkreq;
$("#check-loginname").on("click", function () {
if (checkreq)
checkreq.abort();
$("#user-loginname").removeClass("is-valid");
$("#user-loginname").removeClass("is-invalid");
$("#check-loginname").prop("disabled", true);
checkreq = $.ajax({
url: {{ url_for('.signup_check')|tojson }},
method: "POST",
data: {"loginname": $("#user-loginname").val()},
success: function (resp) {
checkreq = null;
$("#check-loginname").prop("disabled", false);
if (resp.status == "ok") {
$("#user-loginname").addClass("is-valid");
$("#loginname-feedback").text("");
} else if (resp.status == 'exists') {
$("#loginname-feedback").text({{_("The name is already taken")|tojson}});
$("#user-loginname").addClass("is-invalid");
} else if (resp.status == 'ratelimited') {
$("#loginname-feedback").text({{_("Too many requests! Please wait a bit before trying again!")|tojson}});
$("#user-loginname").addClass("is-invalid");
} else {
$("#loginname-feedback").text({{_("The name is invalid")|tojson}});
$("#user-loginname").addClass("is-invalid");
}
}
});
});
$("#user-loginname").on("input", function () {
if (checkreq)
checkreq.abort();
checkreq = null;
$("#user-loginname").removeClass("is-valid");
$("#user-loginname").removeClass("is-invalid");
$("#check-loginname").prop("disabled", false);
});
</script>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
<div class="col-12 mb-3">
<h2 class="text-center">{{_('Confirm your E-Mail Address')}}</h2>
</div>
<p>{{_('We sent a confirmation mail to <b>%(signup_mail)s</b>. You need to confirm your mail address within 48 hours to complete the account registration.', signup_mail=signup.mail|e)|safe}}</p>
<p>{{_("If you mistyped your mail address or don't receive the confirmation mail for another reason, retry the registration procedure from the beginning.")}}</p>
{% endblock %}
...@@ -3,44 +3,47 @@ ...@@ -3,44 +3,47 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("user.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#csvimport">
<i class="fa fa-file-csv" aria-hidden="true"></i> {{_("CSV import")}}
</button>
</p>
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">uid</th> <th scope="col">{{_("UID")}}</th>
<th scope="col">login name</th> <th scope="col">{{_("Login Name")}}</th>
<th scope="col">display name</th> <th scope="col">{{_("Display Name")}}</th>
<th scope="col"> <th scope="col">{{_("Roles")}}</th>
<p class="text-right">
<a type="button" class="btn btn-primary" href="{{ url_for("user.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> New
</a>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#csvimport">
<i class="fa fa-file-csv" aria-hidden="true"></i> CSV import
</button>
</p>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for user in users|sort(attribute="uid") %} {% for user in users|sort(attribute="unix_uid") %}
<tr id="user-{{ user.uid }}"> <tr id="user-{{ user.id }}">
<th scope="row"> <th scope="row">
{{ user.uid }} {{ user.unix_uid }}
</th> </th>
<td> <td>
<a href="{{ url_for("user.show", uid=user.uid) }}"> <a href="{{ url_for("user.show", id=user.id) }}">
{{ user.loginname }} {{ user.loginname }}
</a> </a>
{% if user.is_service_user %}
<span class="badge badge-secondary">{{_('service')}}</span>
{% endif %}
{% if user.is_deactivated %}
<span class="badge badge-danger">{{ _('deactivated') }}</span>
{% endif %}
</td> </td>
<td> <td>
{{ user.displayname }} {{ user.displayname }}
</td> </td>
<td> <td>
<p class="text-right"> {% for role in user.roles|sort(attribute="name") %}
<a href="{{ url_for("user.show", uid=user.uid) }}" class="btn btn-primary"> <a href="{{ url_for("role.show", roleid=role.id) }}">{{ role.name }}</a>{% if not loop.last %}, {% endif %}
<i class="fa fa-edit" aria-hidden="true"></i> Edit {% endfor %}
</a>
</p>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
...@@ -54,16 +57,14 @@ ...@@ -54,16 +57,14 @@
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLabel">Import a csv formated list of users</h5> <h5 class="modal-title" id="exampleModalLabel">{{_("Import a csv formated list of users")}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="{{_('Close')}}">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<p> <p>
The format should be "loginname,mailaddres,groupid1;groupid2;". {{_('The format should be "loginname,mailaddres,roleid1;roleid2". Neither setting the display name nor setting passwords is supported (yet). Example:')}}
Neither setting the display name, nor setting roles or passwords is supported (yet).
Example:
</p> </p>
<pre> <pre>
testuser1,foobar@example.com,5;2;6 testuser1,foobar@example.com,5;2;6
...@@ -72,10 +73,14 @@ testuser5,foobadfar@example.com,0;5;2 ...@@ -72,10 +73,14 @@ testuser5,foobadfar@example.com,0;5;2
testuser2,foobaadsfr@example.com,5;2 testuser2,foobaadsfr@example.com,5;2
</pre> </pre>
<textarea rows="10" class="form-control" name="csv"></textarea> <textarea rows="10" class="form-control" name="csv"></textarea>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="ignore-loginname-blocklist" name="ignore-loginname-blocklist" value="1" aria-label="enabled">
<label class="form-check-label" for="ignore-loginname-blocklist">{{_("Ignore login name blocklist")}}</label>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <button type="button" class="btn btn-secondary" data-dismiss="modal">{{_("Close")}}</button>
<button type="submit" class="btn btn-primary">Import</button> <button type="submit" class="btn btn-primary">{{_("Import")}}</button>
</div> </div>
</div> </div>
</div> </div>
......
{% extends 'base.html' %}
{% macro new_email_row(tmp_id) %}
<tr>
<td>
<input class="form-control form-control-sm" type="email" name="newemail-{{ tmp_id }}-address" placeholder="{{ _('New address') }}">
</td>
<td class="text-center">
<input type="checkbox" value="1" name="newemail-{{ tmp_id }}-verified">
</td>
<td class="text-center">
<button type="button" class="btn btn-sm delete-new-email-row d-none"><i class="fas fa-times"></i></button>
</td>
</tr>
{% endmacro %}
{% block body %}
{% if user.id %}
<form action="{{ url_for("user.update", id=user.id) }}" method="POST" autocomplete="off">
{% else %}
<form action="{{ url_for("user.create") }}" method="POST" autocomplete="off">
{% endif %}
<div class="align-self-center">
{% if user.id and user.is_deactivated %}
<div class="alert alert-warning">
{{ _('This account is deactivated. The user cannot login and existing sessions are not usable. The user cannot log into services, but existing sessions on services might still be active.') }}
</div>
{% endif %}
<div class="clearfix pb-2"><div class="float-sm-right">
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_("Save")}}</button>
<a href="{{ url_for("user.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
{% if user.id and not user.is_deactivated and user != request.user %}
<a href="{{ url_for("user.deactivate", id=user.id) }}" class="btn btn-secondary">{{ _("Deactivate") }}</a>
{% elif user.id and user.is_deactivated %}
<a href="{{ url_for("user.activate", id=user.id) }}" class="btn btn-primary">{{ _("Activate") }}</a>
{% else %}
<a href="#" class="btn btn-secondary disabled">{{ _("Deactivate") }}</a>
{% endif %}
{% if user.id and user != request.user %}
<a href="{{ url_for("user.delete", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% else %}
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% endif %}
</div></div>
<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">{{_("Profile")}}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="roles-tab" data-toggle="tab" href="#roles" role="tab" aria-controls="roles" aria-selected="false">{{_("Roles")}}</a>
</li>
</ul>
<div class="tab-content border mb-2 pt-2" id="tabcontent">
<div class="tab-pane fade show active" id="profile" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<label for="user-uid">
{{_('User ID')}}
{% if user.is_service_user %}
<span class="badge badge-secondary">{{_('service')}}</span>
{% endif %}
</label>
{% if user.id %}
<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.unix_uid }}" readonly>
{% else %}
<input type="text" class="form-control" id="user-uid" name="uid" placeholder="{{_('will be choosen')}}" readonly>
{% endif %}
</div>
{% if not user.id %}
<div class="form-group col">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="user-serviceaccount" name="serviceaccount" value="1" aria-label="enabled">
<label class="form-check-label" for="user-serviceaccount">{{_('Service User')}}</label>
</div>
</div>
{% endif %}
<div class="form-group col">
<label for="user-loginname">{{_("Login Name")}}</label>
<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname or '' }}" {% if user.id %}readonly{% endif %}>
<small class="form-text text-muted">
{{_("Only letters, numbers, dashes (\"-\") and underscores (\"_\") are allowed. At most 32, at least 2 characters. There is a word blocklist. Must be unique.")}}
</small>
</div>
{% if not user.id %}
<div class="form-group col">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="ignore-loginname-blocklist" name="ignore-loginname-blocklist" value="1" aria-label="enabled">
<label class="form-check-label" for="ignore-loginname-blocklist">{{_('Ignore login name blocklist')}}</label>
</div>
</div>
{% endif %}
<div class="form-group col">
<label for="user-loginname">{{_("Display Name")}}</label>
<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname or '' }}">
<small class="form-text text-muted">
{{_("If you leave this empty it will be set to the login name. At most 128, at least 2 characters. No character restrictions.")}}
</small>
</div>
{% if not user.id %}
<div class="form-group col">
<label for="user-email">{{_("E-Mail Address")}}</label>
<input type="email" class="form-control" id="user-email" name="email" value="">
<small class="form-text text-muted">
{{_("Make sure the address is correct! Services might use e-mail addresses as account identifiers and rely on them being unique and verified.")}}
</small>
</div>
{% else %}
<div class="form-group col">
<span>{{_("E-Mail Addresses")}}</span>
<table class="table table-sm mt-2">
<thead>
<tr>
<th scope="col">{{_("Address")}}</th>
<th scope="col" class="text-center">{{_("Verified")}}</th>
<th scope="col" class="text-center">{{_("Delete")}}</th>
</tr>
</thead>
<tbody id="email-rows">
{% for email in user.all_emails %}
<tr>
<td>
<input type="hidden" name="email-{{ email.id }}-present" value="1">
{{ email.address }}
{% if email == user.primary_email %}
<span class="badge badge-primary">{{ _('primary') }}</span>
{% endif %}
</td>
<td class="text-center">
<input type="checkbox" value="1" name="email-{{ email.id }}-verified"{{ ' checked disabled' if email.verified }}>
</td>
<td class="text-center">
<input type="checkbox" value="1" name="email-{{ email.id }}-delete"{{ ' disabled' if email == user.primary_email }}>
</td>
</tr>
{% endfor %}
{{ new_email_row(0) }}
</tbody>
</table>
<small class="form-text text-muted">
{{_("Make sure that addresses you add are correct! Services might use e-mail addresses as account identifiers and rely on them being unique and verified.")}}
</small>
</div>
<div class="form-group col">
<label>{{_("Primary E-Mail Address")}}</label>
<select name="primary_email" class="form-control">
{% for email in user.all_emails if email.verified %}
<option value="{{ email.id }}"{{ ' selected' if email == user.primary_email }}>{{ email.address }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col">
<label>{{_("Recovery E-Mail Address")}}</label>
<select name="recovery_email" class="form-control">
<option value="primary"{{ ' selected' if not user.recovery_email }}>{{ _('Use primary address') }}</option>
{% for email in user.all_emails if email.verified %}
<option value="{{ email.id }}" {{ 'selected' if email == user.recovery_email }}>{{ email.address }}</option>
{% endfor %}
</select>
</div>
{% for service_user in user.service_users if service_user.has_email_preferences %}
<div class="form-group col">
<label>{{ _("Address for %(name)s", name=service_user.service.name) }}</label>
<select name="service_{{ service_user.service.id }}_email" class="form-control">
<option value="primary" {{ 'selected' if not service_user.service_email }}>{{ _('Use primary address') }}</option>
{% for email in user.all_emails if email.verified %}
<option value="{{ email.id }}" {{ 'selected' if email == service_user.service_email }}>{{ email.address }}</option>
{% endfor %}
</select>
</div>
{% endfor %}
{% endif %}
<div class="form-group col">
<label for="user-loginname">{{_("Password")}}</label>
{% if user.id %}
<input type="password" autocomplete="new-password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}">
{% else %}
<input type="text" class="form-control" id="user-password" name="password" placeholder="{{_("E-Mail to set it will be sent")}}" readonly>
{% endif %}
<small class="form-text text-muted">
{{ User.PASSWORD_DESCRIPTION|safe }}
</small>
</div>
{% if user.id %}
<div class="form-group col">
<div class="mb-1">{{_("Two-Factor Authentication")}}</div>
<p>
{{ _("Status:") }} {{ _("Enabled") if user.mfa_enabled else _("Disabled") }}<br>
{{ user.mfa_recovery_codes|length }} {{ _("Recovery Codes") }}, {{ user.mfa_totp_methods|length }} {{ _("Authenticator Apps (TOTP)") }}, {{ user.mfa_webauthn_methods|length }} {{ _("U2F and FIDO2 Devices") }}
</p>
<a href="{{ url_for("user.disable_mfa", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Reset 2FA")}}</a>
</div>
<div class="form-group col">
<div class="mb-1">{{_("Sessions")}}</div>
<p>{{ _("%(session_count)d active sessions", session_count=user.sessions|rejectattr('expired')|list|length) }}</p>
<a href="{{ url_for("user.revoke_sessions", id=user.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Revoke all sessions")}}</a>
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<span>{{_("Roles")}}:</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{_("Name")}}</th>
<th scope="col">{{_("Description")}}</th>
</tr>
</thead>
<tbody>
{% for role in roles|sort(attribute="name") %}
<tr id="role-{{ role.id }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role-{{ role.id }}-checkbox" name="role-{{ role.id }}" value="1" aria-label="enabled"
{% if user in role.members %}checked {% endif %}
{% if role.is_default and not user.is_service_user %}disabled {% endif %}>
</div>
</td>
<td>
<a href="{{ url_for("role.show", roleid=role.id) }}">
{{ role.name }}
</a>
</td>
<td>
{{ role.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="form-group col">
<span>{{_("Resulting groups (only updated after save)")}}:</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">{{_("Name")}}</th>
<th scope="col">{{_("Description")}}</th>
</tr>
</thead>
<tbody>
{% for group in user.groups|sort(attribute="name") %}
<tr id="group-{{ group.id }}">
<td>
<a href="{{ url_for("group.show", id=group.id) }}">
{{ group.name }}
</a>
</td>
<td>
{{ group.description }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</form>
<script>
$('#email-rows').on('click', '.delete-new-email-row', function () {
$(this).closest('tr').remove()
});
let new_email_id = 1;
$('#email-rows').on('input', 'tr:last input', function () {
$('#email-rows tr:last button.delete-new-email-row').removeClass('d-none');
$('#email-rows').append({{ new_email_row('TMPID')|tojson }}.replace(/TMPID/g, new_email_id));
new_email_id ++;
});
</script>
{% endblock %}
File added