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 1342 additions and 41 deletions
...@@ -9,36 +9,122 @@ ...@@ -9,36 +9,122 @@
</div> </div>
{% endif %} {% endif %}
<div class="row mt-3"> <div class="row">
<div class="col-12 col-md-5"> <div class="col-12 col-md-5">
<h5>{{_("Profile")}}</h5> <h5>{{_("Profile")}}</h5>
<p>{{_("Your profile information is used by all services that are integrated into the Single-Sign-On. Your e-mail address is also used for password recovery.")}}</p> <p>{{_("Your profile information is used by all services that are integrated into the Single-Sign-On.")}}</p>
<p>{{_("Changes may take serveral minutes to be visible in all services.")}}</p> <p>{{_("Changes may take several minutes to be visible in all services.")}}</p>
</div> </div>
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
<form class="form" action="{{ url_for("selfservice.update_profile") }}" method="POST"> <form class="form" action="{{ url_for("selfservice.update_profile") }}" method="POST">
<div class="form-row"> <div class="form-group">
<div class="form-group col-12 col-md-9">
<label>{{_("Login Name")}}</label> <label>{{_("Login Name")}}</label>
<input type="text" class="form-control" value="{{ user.loginname }}" readonly> <input type="text" class="form-control" value="{{ user.loginname }}" readonly>
</div> </div>
<div class="form-group col-12 col-md-3">
<label>{{_("User ID")}}</label>
<input type="text" class="form-control" value="{{ user.uid }}" readonly>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label>{{_("Display Name")}}</label> <label>{{_("Display Name")}}</label>
<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}"> <input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}">
</div> </div>
<button type="submit" class="btn btn-primary btn-block">{{_("Update Profile")}}</button>
</form>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-md-5">
<h5>{{_("E-Mail Addresses")}}</h5>
<p>{{_("Add and delete addresses associated with your account. You will need to verify new addresses by opening a link set to them.")}}</p>
</div>
<div class="col-12 col-md-7">
<form method="POST" action="{{ url_for('selfservice.add_email') }}" class="form mb-2">
<div class="row m-0">
<label class="sr-only" for="new-email-address">{{_("Email")}}</label>
<input type="email" autocomplete="email" class="form-control mb-2 col-12 col-lg-auto mr-2" style="width: 20em;" id="new-email-address" name="address" placeholder="{{_("New E-Mail Address")}}" required>
<button type="submit" class="btn btn-primary mb-2 col">{{_("Add address")}}</button>
</div>
</form>
<table class="table mb-0">
<tbody>
{% for email in user.all_emails|sort(attribute='id') %}
<tr>
<td class="pl-0">
{{ email.address }}
{% if email == user.primary_email %}
<span class="badge badge-primary">{{ _('primary') }}</span>
{% elif not email.verified %}
<span class="badge badge-danger">{{ _('unverified') }}</span>
{% endif %}
</td>
<td class="pt-2 pb-1 pr-0">
<form method="POST" action="{{ url_for('selfservice.delete_email', email_id=email.id) }}" onsubmit='return confirm({{_("Are you sure?")|tojson|e}});'>
<button type="submit" class="btn btn-sm btn-danger float-right ml-1 mb-1"{% if email == user.primary_email %} disabled title="{{ _('Cannot delete primary e-mail address') }}"{% endif %}>{{_("Delete")}}</button>
</form>
{% if not email.verified %}
<a href="{{ url_for('selfservice.retry_email_verification', email_id=email.id) }}" class="btn btn-sm btn-primary float-right mb-1">{{_("Retry verification")}}</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<hr>
{% set service_users_with_email_prefs = user.service_users|selectattr('has_email_preferences')|list %}
{% set collapse_email_prefs = service_users_with_email_prefs|length > 2 %}
<div class="row">
<div class="col-12 col-md-5">
<h5>{{_("E-Mail Preferences")}}</h5>
<p>
{{ _("Choose your primary e-mail address and the address password recovery e-mails will be sent to.") }}
{% if service_users_with_email_prefs %}
{{ _("You can also select different addresses for different services.") }}
{% endif %}
</p>
<p>{{ _("Adresses must be verified before you can select them here.") }}</p>
</div>
<div class="col-12 col-md-7">
<form class="form" action="{{ url_for("selfservice.update_email_preferences") }}" method="POST">
<div class="form-group"> <div class="form-group">
<label>{{_("E-Mail Address")}}</label> <label>{{_("Primary Address")}}</label>
<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail }}" required> <select name="primary_email" class="form-control">
<small class="form-text text-muted"> {% for email in user.all_emails if email.verified %}
{{_("We will send you a confirmation mail to this address if you change it")}} <option value="{{ email.id }}" {{ 'selected' if email == user.primary_email }}>{{ email.address }}</option>
</small> {% endfor %}
</select>
</div> </div>
<button type="submit" class="btn btn-primary btn-block">{{_("Update Profile")}}</button> <div class="form-group">
<label>{{_("Address for Password Reset E-Mails")}}</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 service_users_with_email_prefs %}
{% if collapse_email_prefs and loop.index == 2 %}
<div id="collapsed-email-prefs">
{% endif %}
<div class="form-group">
<label>{{ _('Address for Service "%(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 %}
{% if collapse_email_prefs %}
</div>
<button type="button" class="btn btn-sm btn-link pl-0 mb-1 showmore" data-target="#collapsed-email-prefs" style="display: none;" aria-expanded="false" aria-controls="collapsed-email-prefs">{{ _("Show more settings ...") }}</button>
{% endif %}
<button type="submit" class="btn btn-primary btn-block">{{_("Update E-Mail Preferences")}}</button>
</form> </form>
</div> </div>
</div> </div>
...@@ -53,13 +139,13 @@ ...@@ -53,13 +139,13 @@
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
<form class="form" action="{{ url_for("selfservice.change_password") }}" method="POST"> <form class="form" action="{{ url_for("selfservice.change_password") }}" method="POST">
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" id="user-password1" name="password1" placeholder="{{_("New Password")}}" required> <input type="password" autocomplete="new-password" class="form-control" id="user-password1" name="password1" placeholder="{{_("New Password")}}" minlength={{ User.PASSWORD_MINLEN }} maxlength={{ User.PASSWORD_MAXLEN }} pattern="{{ User.PASSWORD_REGEX }}" required>
<small class="form-text text-muted"> <small class="form-text text-muted">
{{_('At least 8 and at most 256 characters, no other special requirements.')}} {{ User.PASSWORD_DESCRIPTION|safe }}
</small> </small>
</div> </div>
<div class="form-group"> <div class="form-group">
<input type="password" class="form-control" id="user-password2" name="password2" placeholder="{{_("Repeat Password")}}" required> <input type="password" autocomplete="new-password" class="form-control" id="user-password2" name="password2" placeholder="{{_("Repeat Password")}}" required>
</div> </div>
<button type="submit" class="btn btn-primary btn-block">{{_("Change Password")}}</button> <button type="submit" class="btn btn-primary btn-block">{{_("Change Password")}}</button>
</form> </form>
...@@ -81,7 +167,56 @@ ...@@ -81,7 +167,56 @@
{{ _("Two-factor authentication is currently <strong>disabled</strong>.")|safe }} {{ _("Two-factor authentication is currently <strong>disabled</strong>.")|safe }}
{% endif %} {% endif %}
</p> </p>
<a class="btn btn-primary btn-block" href="{{ url_for('mfa.setup') }}">{{_("Manage two-factor authentication")}}</a> <a class="btn btn-primary btn-block" href="{{ url_for('selfservice.setup_mfa') }}">{{_("Manage two-factor authentication")}}</a>
</div>
</div>
<hr>
<div class="row mt-3">
<div class="col-12 col-md-5">
<h5>{{_("Active Sessions")}}</h5>
<p>{{_("Your active login sessions on this device and other devices.")}}</p>
<p>{{_("Revoke a session to log yourself out on another device. Note that this is limited to the Single-Sign-On session and <b>does not affect login sessions on services.</b>")}}</p>
</div>
<div class="col-12 col-md-7">
<table class="table">
<thead>
<tr>
<th scope="col">{{_("Last used")}}</th>
<th scope="col">{{_("Device")}}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<tr>
<td>{{_("Just now")}}</td>
<td>{{ request.session.user_agent_browser }} on {{ request.session.user_agent_platform }} ({{ request.session.ip_address }})</td>
<td></td>
</tr>
{% for session in user.sessions|sort(attribute='last_used', reverse=True) if not session.expired and session != request.session %}
<tr>
<td>
{% set last_used_rel = session.last_used - datetime.utcnow() %}
{% if -last_used_rel.total_seconds() <= 60 %}
{{_("Just now")}}
{% else %}
{{ last_used_rel|timedeltaformat(add_direction=True, granularity='minute') }}
{% endif %}
</td>
<td>{{ session.user_agent_browser }} on {{ session.user_agent_platform }} ({{ session.ip_address }})</td>
<td>
{% if session != request.session %}
<form action="{{ url_for("selfservice.revoke_session", session_id=session.id) }}" method="POST" onsubmit='return confirm({{_("Are you sure?")|tojson|e}});'>
<button type="submit" class="btn btn-sm btn-danger float-right">{{_("Revoke")}}</button>
</form>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
</div> </div>
...@@ -92,15 +227,12 @@ ...@@ -92,15 +227,12 @@
<h5>{{_("Roles")}}</h5> <h5>{{_("Roles")}}</h5>
<p>{{_("Aside from a set of base permissions, your roles determine the permissions of your account.")}}</p> <p>{{_("Aside from a set of base permissions, your roles determine the permissions of your account.")}}</p>
{% if config['SERVICES'] %} {% if config['SERVICES'] %}
<p>{{_("See <a href=\"%(services_url)s\">Services</a> for an overview of your current permissions.", services_url=url_for('services.index'))}}</p> <p>{{_("See <a href=\"%(services_url)s\">Services</a> for an overview of your current permissions.", services_url=url_for('service.overview'))}}</p>
{% endif %} {% endif %}
</div> </div>
<div class="col-12 col-md-7"> <div class="col-12 col-md-7">
{% if config['ENABLE_INVITE'] %}
<p>{{_("Administrators and role moderators can invite you to new roles.")}}</p> <p>{{_("Administrators and role moderators can invite you to new roles.")}}</p>
{% else %}
<p>{{_("Administrators can add new roles to your account.")}}</p>
{% endif %}
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
...@@ -119,11 +251,9 @@ ...@@ -119,11 +251,9 @@
</td> </td>
<td>{{ role.description }}</td> <td>{{ role.description }}</td>
<td> <td>
{% if config['ENABLE_ROLESELFSERVICE'] %}
<form action="{{ url_for("selfservice.leave_role", roleid=role.id) }}" method="POST" onsubmit='return confirm({{_("Are you sure?")|tojson|e}});'> <form action="{{ url_for("selfservice.leave_role", roleid=role.id) }}" method="POST" onsubmit='return confirm({{_("Are you sure?")|tojson|e}});'>
<button type="submit" class="btn btn-sm btn-danger float-right">{{_("Leave")}}</button> <button type="submit" class="btn btn-sm btn-danger float-right">{{_("Leave")}}</button>
</form> </form>
{% endif %}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
...@@ -134,7 +264,20 @@ ...@@ -134,7 +264,20 @@
{% endif %} {% endif %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<script>
$(".showmore").each(function () {
$(this).show()
$($(this).data("target")).hide()
})
$(".showmore").on("click", function () {
$(this).slideUp(200)
$(this).prop("ariaExpanded", true)
$($(this).data("target")).slideDown()
})
</script>
{% endblock %} {% endblock %}
{% 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 %}
...@@ -28,11 +28,11 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -28,11 +28,11 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
{% 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 %}
...@@ -56,7 +56,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -56,7 +56,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
</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"> <button type="submit" class="btn btn-primary mb-2 col">
{{_("Generate recovery codes to enable two-factor authentication")}} {{_("Generate recovery codes to enable two-factor authentication")}}
...@@ -93,7 +93,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -93,7 +93,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
</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 }}>
...@@ -113,8 +113,8 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -113,8 +113,8 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
{% for method in request.user.mfa_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 request.user.mfa_totp_methods %} {% if not request.user.mfa_totp_methods %}
...@@ -152,7 +152,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -152,7 +152,7 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
</div> </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>
...@@ -175,8 +175,8 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -175,8 +175,8 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
{% for method in request.user.mfa_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 request.user.mfa_webauthn_methods %} {% if not request.user.mfa_webauthn_methods %}
...@@ -196,21 +196,21 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe ...@@ -196,21 +196,21 @@ mfa_enabled: The user has setup at least one two-factor method. Two-factor authe
$('#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({
...@@ -222,34 +222,34 @@ $('#webauthn-form').on('submit', function(e) { ...@@ -222,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;
...@@ -261,7 +261,7 @@ if (typeof(PublicKeyCredential) != "undefined") { ...@@ -261,7 +261,7 @@ 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');
} }
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<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>
...@@ -23,8 +23,8 @@ ...@@ -23,8 +23,8 @@
</p> </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"> <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">
{{_("Download codes")}} {{_("Download codes")}}
</a> </a>
<button class="ml-2 mb-2 btn btn-light d-print-none" type="button" onClick="window.print()">{{_("Print codes")}}</button> <button class="ml-2 mb-2 btn btn-light d-print-none" type="button" onClick="window.print()">{{_("Print codes")}}</button>
......
...@@ -32,9 +32,9 @@ ...@@ -32,9 +32,9 @@
</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 %}
...@@ -4,8 +4,19 @@ ...@@ -4,8 +4,19 @@
{% set iconstyle = 'style="width: 1.8em;"'|safe %} {% set iconstyle = 'style="width: 1.8em;"'|safe %}
{% if not user %} {% if not request.user %}
<div class="alert alert-warning" role="alert">{{_("Some services may not be publicly listed! Log in to see all services you have access to.")}}</div> <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 %} {% endif %}
{% if banner %} {% if banner %}
...@@ -22,7 +33,7 @@ ...@@ -22,7 +33,7 @@
<div class="card-body"> <div class="card-body">
{% if service.logo_url %} {% if service.logo_url %}
{% if service.url and service.has_access %}<a href="{{ service.url }}" class="text-reset">{% endif %} {% if service.url and service.has_access %}<a href="{{ service.url }}" class="text-reset">{% endif %}
<img alt="Logo for {{ service.title }}" src="{{ service.logo_url }}" style="width: 100%; height: 10em; object-fit: contain; {{ 'filter: grayscale(100%);' if not service.has_access }}"> <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 %} {% if service.url and service.has_access %}</a>{% endif %}
{% endif %} {% endif %}
<h5 class="card-title"> <h5 class="card-title">
...@@ -59,6 +70,12 @@ ...@@ -59,6 +70,12 @@
</div> </div>
{% endmacro %} {% 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"> <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 %} {% for service in services if service.has_access %}
{{ service_card(service) }} {{ service_card(service) }}
......
{% 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="row mt-2 justify-content-center">
<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 class="text-center">
<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12 mb-3"> <div class="col-12 mb-3">
<h2 class="text-center">{{_("Two-Factor Authentication")}}</h2> <h2 class="text-center">{{_("Two-Factor Authentication")}}</h2>
</div> </div>
...@@ -30,7 +24,7 @@ ...@@ -30,7 +24,7 @@
<div class="text-center text-muted d-none webauthn-group mb-3">- {{_("or")}} -</div> <div class="text-center text-muted d-none webauthn-group mb-3">- {{_("or")}} -</div>
{% endif %} {% endif %}
<div class="form-group col-12 mb-2"> <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> <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>
<div class="form-group col-12"> <div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_("Verify")}}</button> <button type="submit" class="btn btn-primary btn-block">{{_("Verify")}}</button>
...@@ -38,8 +32,6 @@ ...@@ -38,8 +32,6 @@
<div class="form-group col-12"> <div class="form-group col-12">
<a href="{{ url_for("session.logout") }}" class="btn btn-secondary btn-block">{{_("Cancel")}}</a> <a href="{{ url_for("session.logout") }}" class="btn btn-secondary btn-block">{{_("Cancel")}}</a>
</div> </div>
</div>
</div>
</form> </form>
{% if webauthn_supported and request.user_pre_mfa.mfa_webauthn_methods %} {% if webauthn_supported and request.user_pre_mfa.mfa_webauthn_methods %}
...@@ -48,27 +40,27 @@ ...@@ -48,27 +40,27 @@
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) { if (response.ok) {
return response.arrayBuffer(); return response.arrayBuffer();
} else if (response.status == 403) { } else if (response.status == 403) {
window.location = {{ request.url|tojson }}; /* reload */ window.location = {{ request.url|tojson }}; /* reload */
throw new Error('Session timed out'); throw new Error({{ _('Session timed out')|tojson }});
} else if (response.status == 404) { } else if (response.status == 404) {
throw new Error('You have not registered any U2F/FIDO2 devices for your account'); throw new Error({{ _('You have not registered any U2F/FIDO2 devices for your account')|tojson }});
} else { } else {
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.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({
...@@ -81,37 +73,37 @@ function begin_webauthn() { ...@@ -81,37 +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) { } else if (response.status == 403) {
window.location = {{ request.url|tojson }}; /* reload */ window.location = {{ request.url|tojson }}; /* reload */
throw new Error('Session timed out'); throw new Error({{ _('Session timed out')|tojson }});
} else { } else {
throw new Error('Response from authenticator rejected'); throw new Error({{ _('Invalid response from device')|tojson }});
} }
}).catch(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 %}
...@@ -4,7 +4,7 @@ an account was created on the {{ config.ORGANISATION_NAME }} infrastructure with ...@@ -4,7 +4,7 @@ an account was created on the {{ config.ORGANISATION_NAME }} infrastructure with
Please visit the following url to complete the account registration: Please visit the following url to complete the account registration:
{{ url_for('signup.signup_confirm', token=signup.token, _external=True) }} {{ url_for('signup.signup_confirm', signup_id=signup.id, token=signup.token, _external=True) }}
**The link is valid for 48h** **The link is valid for 48h**
......
{% extends 'base.html' %} {% extends 'base_narrow.html' %}
{% block body %} {% block body %}
<form action="{{ url_for('.signup_submit') }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '') "> <form action="{{ url_for('.signup_submit') }}" method="POST" onInput="password2.setCustomValidity(password1.value != password2.value ? 'Passwords do not match.' : '') ">
<div class="row mt-2 justify-content-center">
<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 class="text-center">
<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12"> <div class="col-12">
<h2 class="text-center">{{_('Account Registration')}}</h2> <h2 class="text-center">{{_('Account Registration')}}</h2>
</div> </div>
{% if error %}
<div class="form-group col-12">
<div class="alert alert-danger" role="alert">{{ error }}</div>
</div>
{% endif %}
<div class="form-group col-12"> <div class="form-group col-12">
<label for="user-loginname">{{_('Login Name')}}</label> <label for="user-loginname">{{_('Login Name')}}</label>
<div class="js-only-input-group"> <div class="js-only-input-group">
<input type="text" 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> <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"> <div class="js-only-input-group-append d-none">
<button class="btn btn-outline-secondary rounded-right" type="button" id="check-loginname">{{_('Check')}}</button> <button class="btn btn-outline-secondary rounded-right" type="button" id="check-loginname">{{_('Check')}}</button>
</div> </div>
...@@ -30,34 +20,32 @@ ...@@ -30,34 +20,32 @@
</div> </div>
<div class="form-group col-12"> <div class="form-group col-12">
<label for="user-displayname">{{_('Display Name')}}</label> <label for="user-displayname">{{_('Display Name')}}</label>
<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ request.form.displayname }}" minlength=1 maxlength=128 required> <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"> <small class="form-text text-muted">
{{_('At least one and at most 128 characters, no other special requirements.')}} {{_('At least one and at most 128 characters, no other special requirements.')}}
</small> </small>
</div> </div>
<div class="form-group col-12"> <div class="form-group col-12">
<label for="user-mail">{{_('E-Mail Address')}}</label> <label for="user-mail">{{_('E-Mail Address')}}</label>
<input type="email" class="form-control" id="user-mail" name="mail" value="{{ request.form.mail }}" required> <input type="email" autocomplete="email" class="form-control" id="user-mail" name="mail" value="{{ request.form.mail }}" required>
<small class="form-text text-muted"> <small class="form-text text-muted">
{{_('We will send a confirmation mail to this address that you need to complete the registration.')}} {{_('We will send a confirmation mail to this address that you need to complete the registration.')}}
</small> </small>
</div> </div>
<div class="form-group col-12"> <div class="form-group col-12">
<label for="user-password1">{{_('Password')}}</label> <label for="user-password1">{{_('Password')}}</label>
<input type="password" class="form-control" id="user-password1" name="password1" minlength=8 maxlength=256 required> <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"> <small class="form-text text-muted">
{{_("At least 8 and at most 256 characters, no other special requirements. But please don't be stupid, do use a password manager.")}} {{ User.PASSWORD_DESCRIPTION|safe }}
</small> </small>
</div> </div>
<div class="form-group col-12"> <div class="form-group col-12">
<label for="user-password2">{{_('Repeat Password')}}</label> <label for="user-password2">{{_('Repeat Password')}}</label>
<input type="password" class="form-control" id="user-password2" name="password2" required> <input type="password" autocomplete="new-password" class="form-control" id="user-password2" name="password2" required>
</div> </div>
<div class="form-group col-12"> <div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block">{{_('Create Account')}}</button> <button type="submit" class="btn btn-primary btn-block">{{_('Create Account')}}</button>
</div> </div>
</div>
</div>
</form> </form>
<script> <script>
......
{% 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 %}
...@@ -21,18 +21,21 @@ ...@@ -21,18 +21,21 @@
</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 %} {% if user.is_service_user %}
<span class="badge badge-secondary">{{_('service')}}</span> <span class="badge badge-secondary">{{_('service')}}</span>
{% endif %} {% endif %}
{% if user.is_deactivated %}
<span class="badge badge-danger">{{ _('deactivated') }}</span>
{% endif %}
</td> </td>
<td> <td>
{{ user.displayname }} {{ user.displayname }}
...@@ -71,8 +74,8 @@ testuser2,foobaadsfr@example.com,5;2 ...@@ -71,8 +74,8 @@ 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"> <div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="ignore-loginname-blacklist" name="ignore-loginname-blacklist" value="1" aria-label="enabled"> <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-blacklist">{{_("Ignore login name blacklist")}}</label> <label class="form-check-label" for="ignore-loginname-blocklist">{{_("Ignore login name blocklist")}}</label>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
......
{% extends 'base.html' %} {% 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 %} {% block body %}
<form action="{{ url_for("user.update", uid=user.uid) }}" method="POST"> {% 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"> <div class="align-self-center">
<div class="float-sm-right pb-2"> {% 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> <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> <a href="{{ url_for("user.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
{% if user.uid %} {% if user.id and not user.is_deactivated and user != request.user %}
<a href="{{ url_for("mfa.admin_disable", uid=user.uid) }}" class="btn btn-secondary">{{_("Reset 2FA")}}</a> <a href="{{ url_for("user.deactivate", id=user.id) }}" class="btn btn-secondary">{{ _("Deactivate") }}</a>
<a href="{{ url_for("user.delete", uid=user.uid) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</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 %} {% else %}
<a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a> <a href="#" class="btn btn-danger disabled"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% endif %} {% endif %}
</div> </div></div>
<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist"> <ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
<li class="nav-item"> <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> <a class="nav-link active" id="profile-tab" data-toggle="tab" href="#profile" role="tab" aria-controls="profile" aria-selected="true">{{_("Profile")}}</a>
...@@ -30,13 +60,13 @@ ...@@ -30,13 +60,13 @@
<span class="badge badge-secondary">{{_('service')}}</span> <span class="badge badge-secondary">{{_('service')}}</span>
{% endif %} {% endif %}
</label> </label>
{% if user.uid %} {% if user.id %}
<input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.uid or '' }}" readonly> <input type="number" class="form-control" id="user-uid" name="uid" value="{{ user.unix_uid }}" readonly>
{% else %} {% else %}
<input type="text" class="form-control" id="user-uid" name="uid" placeholder="{{_('will be choosen')}}" readonly> <input type="text" class="form-control" id="user-uid" name="uid" placeholder="{{_('will be choosen')}}" readonly>
{% endif %} {% endif %}
</div> </div>
{% if not user.uid %} {% if not user.id %}
<div class="form-group col"> <div class="form-group col">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="user-serviceaccount" name="serviceaccount" value="1" aria-label="enabled"> <input class="form-check-input" type="checkbox" id="user-serviceaccount" name="serviceaccount" value="1" aria-label="enabled">
...@@ -46,16 +76,16 @@ ...@@ -46,16 +76,16 @@
{% endif %} {% endif %}
<div class="form-group col"> <div class="form-group col">
<label for="user-loginname">{{_("Login Name")}}</label> <label for="user-loginname">{{_("Login Name")}}</label>
<input type="text" class="form-control" id="user-loginname" name="loginname" value="{{ user.loginname or '' }}" {% if user.uid %}readonly{% endif %}> <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"> <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 blacklist. Must be unique.")}} {{_("Only letters, numbers, dashes (\"-\") and underscores (\"_\") are allowed. At most 32, at least 2 characters. There is a word blocklist. Must be unique.")}}
</small> </small>
</div> </div>
{% if not user.uid %} {% if not user.id %}
<div class="form-group col"> <div class="form-group col">
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="checkbox" id="ignore-loginname-blacklist" name="ignore-loginname-blacklist" value="1" aria-label="enabled"> <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-blacklist">{{_('Ignore login name blacklist')}}</label> <label class="form-check-label" for="ignore-loginname-blocklist">{{_('Ignore login name blocklist')}}</label>
</div> </div>
</div> </div>
{% endif %} {% endif %}
...@@ -66,24 +96,111 @@ ...@@ -66,24 +96,111 @@
{{_("If you leave this empty it will be set to the login name. At most 128, at least 2 characters. No character restrictions.")}} {{_("If you leave this empty it will be set to the login name. At most 128, at least 2 characters. No character restrictions.")}}
</small> </small>
</div> </div>
{% if not user.id %}
<div class="form-group col"> <div class="form-group col">
<label for="user-mail">{{_("Mail")}}</label> <label for="user-email">{{_("E-Mail Address")}}</label>
<input type="email" class="form-control" id="user-mail" name="mail" value="{{ user.mail or '' }}"> <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"> <small class="form-text text-muted">
{{_("Do a sanity check here. A user can take over another account if both have the same mail address set.")}} {{_("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> </small>
</div> </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"> <div class="form-group col">
<label for="user-loginname">{{_("Password")}}</label> <label for="user-loginname">{{_("Password")}}</label>
{% if user.uid %} {% if user.id %}
<input type="password" class="form-control" id="user-password" name="password" placeholder="●●●●●●●●"> <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 %} {% else %}
<input type="password" class="form-control" id="user-password" name="password" placeholder="{{_("mail to set it will be sent")}}" readonly> <input type="text" class="form-control" id="user-password" name="password" placeholder="{{_("E-Mail to set it will be sent")}}" readonly>
{% endif %} {% endif %}
<small class="form-text text-muted"> <small class="form-text text-muted">
{{_("At least 8 and at most 256 characters, no other special requirements.")}} {{ User.PASSWORD_DESCRIPTION|safe }}
</small> </small>
</div> </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>
<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab"> <div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col"> <div class="form-group col">
...@@ -130,9 +247,9 @@ ...@@ -130,9 +247,9 @@
</thead> </thead>
<tbody> <tbody>
{% for group in user.groups|sort(attribute="name") %} {% for group in user.groups|sort(attribute="name") %}
<tr id="group-{{ group.gid }}"> <tr id="group-{{ group.id }}">
<td> <td>
<a href="{{ url_for("group.show", gid=group.gid) }}"> <a href="{{ url_for("group.show", id=group.id) }}">
{{ group.name }} {{ group.name }}
</a> </a>
</td> </td>
...@@ -148,4 +265,17 @@ ...@@ -148,4 +265,17 @@
</div> </div>
</div> </div>
</form> </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 %} {% endblock %}