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
  • Dockerfile
  • feature_invite_validuntil_minmax
  • incremental-sync
  • jwt_encode_inconsistencies
  • master
  • redis-rate-limits
  • roles-recursive-cte
  • typehints
  • v1.0.x
  • v1.1.x
  • v1.2.x
  • v1.x.x
  • v0.1.2
  • v0.1.4
  • v0.1.5
  • v0.2.0
  • v0.3.0
  • v1.0.0
  • v1.0.1
  • v1.0.2
  • v1.1.0
  • v1.1.1
  • v1.1.2
  • v1.2.0
  • v2.0.0
  • v2.0.1
  • v2.1.0
  • v2.2.0
  • v2.3.0
  • v2.3.1
30 results

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 1352 additions and 22 deletions
{% extends 'base.html' %}
{% block body %}
<div class="btn-toolbar mb-2">
<a class="btn btn-primary ml-auto" href="{{ url_for("invite.new") }}"><i class="fa fa-plus" aria-hidden="true"></i> {{_('New')}}</a>
</div>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th scope="col">{{_('Link')}}</th>
<th scope="col">{{_('Created by')}}</th>
<th scope="col">{{_('Permissions')}}</th>
<th scope="col">{{_('Usages')}}</th>
<th scope="col">{{_('Status')}}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for invite in invites|sort(attribute='created', reverse=True)|sort(attribute='active', reverse=True) %}
<tr>
<td>
{% if invite.creator == request.user and invite.active %}
<a href="{{ url_for('invite.use', invite_id=invite.id, token=invite.token) }}"><code>{{ invite.short_token }}</code></a>
<button type="button" class="btn btn-link btn-sm p-0 copy-clipboard" data-copy="{{ url_for('invite.use', invite_id=invite.id, token=invite.token, _external=True) }}" title="{{_('Copy link to clipboard')}}"><i class="fas fa-clipboard"></i></button>
<button type="button" class="btn btn-link btn-sm p-0" data-toggle="modal" data-target="#modal-{{ invite.id }}-qrcode" title="{{_('Show link as QR code')}}"><i class="fas fa-qrcode"></i></button>
{% else %}
<code>{{ invite.short_token }}</code>
{% endif %}
</td>
<td>
{% if not invite.creator %}
{{ '<deleted user>' }}
{% else %}
{{ invite.creator.loginname }}
{% endif %}
</td>
<td>
{{ _('Signup') if invite.allow_signup }}{{ ', ' if invite.allow_signup and invite.roles }}
{% for role in invite.roles %}{{ ', ' if loop.index != 1 }}<i class="fas fa-key"></i>&thinsp;{{ role.name }}{% endfor %}
</td>
<td>
<span style="white-space: nowrap;">{{ invite.signups|selectattr('completed')|list|length }} <i class="fas fa-users" title="{{ _('user signups') }}"></i></span>,
<span style="white-space: nowrap;">{{ invite.grants|length }} <i class="fas fa-key" title="role grants"></i></span>
</td>
<td>
{% if invite.disabled %}
{{_('Disabled')}}
{% elif invite.voided %}
{{_('Voided')}}
{% elif invite.expired %}
{{_('Expired')}}
{% elif not invite.permitted %}
{{_('Invalid, unpermitted creator')}}
{% elif not invite.active %}
{{_('Invalid')}}
{% elif invite.single_use %}
{{ _('Valid once, expires %(expiry_date)s', expiry_date=invite.valid_until|dateformat) }}
{% else %}
{{ _('Valid, expires %(expiry_date)s', expiry_date=invite.valid_until|dateformat) }}
{% endif %}
</td>
<td class="text-right">
<button type="button" class="btn btn-link btn-sm p-0" data-toggle="modal" data-target="#modal-{{ invite.id }}"><i class="fas fa-ellipsis-h"></i></button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% for invite in invites %}
<div class="modal" tabindex="-1" id="modal-{{ invite.id }}">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{_('Invite Link Details')}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<ul class="list-unstyled">
<li><b>{{_('Type:')}}</b> {% if invite.single_use %}{{_('Single-use')}}{% else %}{{_('Multi-use')}}{% endif %}</li>
<li><b>{{_('Created:')}}</b> {{ invite.created|datetimeformat }}</li>
<li><b>{{_('Expires:')}}</b> {{ invite.valid_until|datetimeformat }}</li>
<li><b>{{_('Permissions:')}}</b>
<ul>
{% if invite.allow_signup %}
<li>{{_('Link allows account registration')}}</li>
{% else %}
<li>{{_('No account registration allowed')}}</li>
{% endif %}
{% for role in invite.roles %}
<li>{{_('Link grants users the role "%(name)s"', name=role.name)}}</li>
{% endfor %}
</ul>
</li>
<li><b>Usages:</b>
{% if not invite.signups and not invite.grants %}
{{_('Never used')}}
{% else %}
<ul>
{% for signup in invite.signups if signup.completed %}
<li>{{_('Registration of user <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', id=signup.user.id)|e, user_name=signup.user.loginname|e)|safe}}</li>
{% endfor %}
{% for grant in invite.grants if grant.user %}
<li>{{_('Roles granted to <a href="%(user_url)s">%(user_name)s</a>', user_url=url_for('user.show', id=grant.user.id)|e, user_name=grant.user.loginname|e)|safe}}</li>
{% endfor %}
</ul>
{% endif %}
</li>
</ul>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
{% if invite.active %}
<form action="{{ url_for('invite.disable', invite_id=invite.id) }}" method="POST">
<button type="submit" class="btn btn-primary">{{_('Disable Link')}}</button>
</form>
{% elif invite.creator == request.user and not invite.expired and invite.permitted %}
<form action="{{ url_for('invite.reset', invite_id=invite.id) }}" method="POST">
<button type="submit" class="btn btn-primary">{{_('Reenable Link')}}</button>
</form>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
{% for invite in invites if invite.creator == request.user %}
<div class="modal" tabindex="-1" id="modal-{{ invite.id }}-qrcode">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{{_('Invite')}}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{{ url_for('invite.use', invite_id=invite.id, token=invite.token, _external=True)|qrcode_svg(width='100%', height='100%') }}
</div>
</div>
</div>
</div>
{% endfor %}
<script>
$(".copy-clipboard").on("click", function() {
navigator.clipboard.writeText($(this).data("copy"));
});
</script>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<form action="{{ url_for("invite.new_submit") }}" method="POST" autocomplete="off" class="form">
<div class="form-group">
<label for="single-use">{{_('Link Type')}}</label>
<select class="form-control" id="single-use" name="single-use">
<option value="1" {{ 'selected' if request.values.get('single-use', '1') == '1' }}>{{_('Valid for a single successful use')}}</option>
<option value="0" {{ 'selected' if request.values.get('single-use', '1') == '0' }}>{{_('Multi-use')}}</option>
</select>
</div>
<div class="form-group">
<label for="valid-until">{{_('Valid Until')}}</label>
<input class="form-control" type="datetime-local" id="valid-until" name="valid-until" value="{{ request.values.get('valid-until') or (datetime.now() + timedelta(hours=36)).replace(hour=23, minute=59).isoformat(timespec='minutes') }}" min="{{ datetime.now().isoformat(timespec='minutes') }}" max="{{ (datetime.now() + timedelta(days=config['INVITE_MAX_VALID_DAYS'])).isoformat(timespec='minutes') }}">
<small class="text-muted">{{_('Must be within the next %(max_valid_days)d days', max_valid_days=config['INVITE_MAX_VALID_DAYS'])}}</small>
</div>
{% if allow_signup %}
<div class="form-group">
<label for="allow-signup">{{_('Account Registration')}}</label>
<select class="form-control" id="allow-signup" name="allow-signup">
<option value="1" {{ 'selected' if request.values.get('allow-signup', '1') == '1' }}>{{_('Link allows account registration')}}</option>
<option value="0" {{ 'selected' if request.values.get('allow-signup', '1') == '0' }}>{{_('No account registration allowed')}}</option>
</select>
</div>
{% else %}
<input type="hidden" name="allow-signup" value="0">
{% endif %}
{% if roles %}
<div class="form-group">
<label for="valid-until">{{_('Granted Roles')}}</label>
<table class="table 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") if not role.is_default %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="role-{{ role.id }}" name="role-{{ role.id }}" value="1" {{ 'checked' if 'role-%d'%role.id in request.values }}>
</div>
</td>
<td>{{ role.name }}</td>
<td>{{ role.description }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_('Create Link')}}</button>
<a href="{{ url_for("invite.index") }}" class="btn btn-secondary">{{_('Cancel')}}</a>
</form>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
<div class="col-12 mb-3">
<h2 class="text-center">{{_('Invite Link')}}</h2>
</div>
{% if not request.user %}
<p>{{_('Welcome to the %(org_name)s Single-Sign-On!', org_name=config.ORGANISATION_NAME)}}</p>
{% endif %}
{% if invite.roles and invite.allow_signup %}
<p>{{_('With this link you can register a new user account with the following roles or add the roles to an existing account:')}}</p>
{% elif invite.roles %}
<p>{{_('With this link you can add the following roles to an existing account:')}}</p>
{% elif invite.allow_signup %}
<p>{{_('With this link you can register a new user account.')}}</p>
{% endif %}
{% if invite.roles %}
<ul>
{% for role in invite.roles %}
<li>{{ role.name }}{% if role.description %}: {{ role.description }}{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
{% if request.user %}
{% if invite.roles %}
<form method="POST" action="{{ url_for("invite.grant", invite_id=invite.id, token=invite.token) }}" class="mb-2">
<button type="submit" class="btn btn-primary btn-block">{{_('Add the roles to your account now')}}</button>
</form>
<a href="{{ url_for("session.logout", ref=url_for("session.login", ref=request.full_path)) }}" class="btn btn-secondary btn-block">{{_('Logout and switch to a different account')}}</a>
{% endif %}
{% if invite.allow_signup %}
<a href="{{ url_for("session.logout", ref=url_for("invite.signup_start", invite_id=invite.id, token=invite.token)) }}" class="btn btn-secondary btn-block">{{_('Logout to register a new account')}}</a>
{% endif %}
{% else %}
{% if invite.allow_signup %}
<a href="{{ url_for("invite.signup_start", invite_id=invite.id, token=invite.token) }}" class="btn btn-primary btn-block">{{_('Register a new account')}}</a>
{% endif %}
{% if invite.roles %}
<a href="{{ url_for("session.login", ref=request.full_path) }}" class="btn btn-primary btn-block">{{_('Login and add the roles to your account')}}</a>
{% endif %}
{% endif %}
{% endblock %}
...@@ -3,50 +3,41 @@ ...@@ -3,50 +3,41 @@
{% 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("mail.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_('New')}}
</a>
</p>
<table class="table table-striped table-sm"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">name</th> <th scope="col">{{_('Name')}}</th>
<th scope="col">receiving address</th> <th scope="col">{{_('Receiving addresses')}}</th>
<th scope="col">destinations</th> <th scope="col">{{_('Destinations')}}</th>
<th scope="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("mail.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> New
</a>
</p>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for mail in mails|sort(attribute="uid") %} {% for mail in mails|sort(attribute="uid") %}
<tr id="mail-{{ mail.uid }}"> <tr id="mail-{{ mail.id }}">
<th scope="row"> <th scope="row">
<a href="{{ url_for("mail.show", uid=mail.uid) }}"> <a href="{{ url_for("mail.show", mail_id=mail.id) }}">
{{ mail.uid }} {{ mail.uid }}
</a> </a>
</th> </th>
<td> <td>
<ul> <ul class="m-0">
{% for i in mail.receivers %} {% for i in mail.receivers %}
<li>{{ i }}</li> <li>{{ i }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td> <td>
<ul> <ul class="m-0">
{% for i in mail.destinations %} {% for i in mail.destinations %}
<li>{{ i }}</li> <li>{{ i }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
</td> </td>
<td>
<p class="text-right">
<a href="{{ url_for("mail.show", uid=mail.uid) }}" class="btn btn-primary">
<i class="fa fa-edit" aria-hidden="true"></i> Edit
</a>
</p>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
......
{% extends 'base.html' %} {% extends 'base.html' %}
{% block body %} {% block body %}
<form action="{{ url_for("mail.update", uid=mail.uid) }}" method="POST"> <form action="{{ url_for("mail.update", mail_id=mail.id) }}" method="POST" autocomplete="off">
<div class="align-self-center"> <div class="align-self-center">
<div class="form-group col"> <div class="form-group col">
<label for="mail-name">Name</label> <label for="mail-name">{{_('Name')}}</label>
<input type="text" class="form-control" id="mail-name" name="mail-uid" {% if mail.uid %} value="{{ mail.uid }}" readonly {% else %} value=""{% endif %}> <input type="text" class="form-control" id="mail-name" name="mail-uid" {% if mail.id %} value="{{ mail.uid }}" readonly {% else %} value=""{% endif %}>
<small class="form-text text-muted"> <small class="form-text text-muted">
</small> </small>
</div> </div>
<div class="form-group col"> <div class="form-group col">
<label for="mail-receivers">Receiving addresses</label> <label for="mail-receivers">{{_('Receiving addresses')}}</label>
<textarea rows="10" class="form-control" id="mail-receivers" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea> <textarea rows="10" class="form-control" id="mail-receivers" name="mail-receivers">{{ mail.receivers|join('\n') }}</textarea>
<small class="form-text text-muted"> <small class="form-text text-muted">
One address per line {{_('One address pattern (local+ext@domain, local@domain, local, @domain) per line. Only lower-case ASCII letters, digits and symbols.')}}
</small> </small>
</div> </div>
<div class="form-group col"> <div class="form-group col">
<label for="mail-destinations">Destinations</label> <label for="mail-destinations">{{_('Destinations')}}</label>
<textarea rows="10" class="form-control" id="mail-destinations" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea> <textarea rows="10" class="form-control" id="mail-destinations" name="mail-destinations">{{ mail.destinations|join('\n') }}</textarea>
<small class="form-text text-muted"> <small class="form-text text-muted">
One address per line {{_('One address per line')}}
</small> </small>
</div> </div>
<div class="form-group col"> <div class="form-group col">
<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("mail.index") }}" class="btn btn-secondary">Cancel</a> <a href="{{ url_for("mail.index") }}" class="btn btn-secondary">{{_('Cancel')}}</a>
{% if mail.uid %} {% if mail.id %}
<a href="{{ url_for("mail.delete", uid=mail.uid) }}" class="btn btn-danger"><i class="fa fa-trash" aria-hidden="true"></i> Delete</a> <a href="{{ url_for("mail.delete", mail_id=mail.id) }}" 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> </div>
......
...@@ -3,14 +3,6 @@ ...@@ -3,14 +3,6 @@
{% block body %} {% block body %}
<h1>OAuth2.0 Authorization Error</h1> <h1>OAuth2.0 Authorization Error</h1>
<p><b>Error: {{ error }}</b> {{ '(' + error_description + ')' if error_description else '' }}</p> <p><b>Error: {{ error }}</b> {{ '(' + error_description + ')' if error_description else '' }}</p>
{% if args %}
<p>Parameters:</p>
<ul>
{% for key, value in args.items() %}
<li>{{ key }}={{ value }}</li>
{% endfor %}
</ul>
{% endif %}
<hr> <hr>
......
{% extends 'base.html' %} {% extends 'base_narrow.html' %}
{% block body %} {% block body %}
<div class="row mt-2 justify-content-center"> <div class="col-12">
<div class="col-lg-6 col-md-10" style="background: #f7f7f7; box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3); padding: 30px;"> <h2 class="text-center">{{_('Logout')}}</h2>
<div class="text-center"> </div>
<img alt="branding logo" src="{{ config.get("BRANDING_LOGO_URL") }}" class="col-lg-8 col-md-12" >
</div>
<div class="col-12">
<h2 class="text-center">Logout</h2>
</div>
<div class="col-12"> <div class="col-12">
<noscript> <noscript>
<div class="alert alert-warning" role="alert">Javascript is required for automatic logout</div> <div class="alert alert-warning" role="alert">{{_('Javascript is required for automatic logout')}}</div>
</noscript> </noscript>
<p>While you successfully logged out of the Single-Sign-On service, you may still be logged in on these services:</p> <p>{{_('While you successfully logged out of the Single-Sign-On service, you may still be logged in on these services:')}}</p>
<ul> <ul>
{% for client in clients if client.logout_urls %} {% for client in clients if client.logout_uris %}
<li class="client" data-urls='{{ client.logout_urls|tojson }}'> <li class="client" data-urls='{{ client.logout_uris_json }}'>
{{ client.client_id }} {{ client.service.name }}
<span class="status-active spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span> <span class="status-active spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
<i class="status-success fas fa-check d-none"></i> <i class="status-success fas fa-check d-none"></i>
<i class="status-failed fas fa-exclamation d-none"></i> <i class="status-failed fas fa-exclamation d-none"></i>
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<p> <p>
Please wait until you have been automatically logged out of all services or make sure of this yourself. {{_('Please wait until you have been automatically logged out of all services or make sure of this yourself.')}}
</p> </p>
<button id="retry-button" class="btn btn-block btn-primary d-none" disabled> <button id="retry-button" class="btn btn-block btn-primary d-none" disabled>
<span id="cont-text">Logging you out on all services ...</span> <span id="cont-text">{{_('Logging you out on all services ...')}}</span>
</button> </button>
<a href="{{ request.values.get('ref') or '/' }}" class="btn btn-block btn-secondary"> <a href="{{ request.values.get('ref') or '/' }}" class="btn btn-block btn-secondary">
<span>Skip this and continue</span> <span>{{_('Skip this and continue')}}</span>
</a> </a>
</div>
</div>
</div> </div>
<script> <script>
...@@ -60,7 +53,6 @@ function logout_services() { ...@@ -60,7 +53,6 @@ function logout_services() {
}); });
}); });
p = p.then(function () { p = p.then(function () {
console.log('done', elem);
elem.find('.status-active').addClass('d-none'); elem.find('.status-active').addClass('d-none');
elem.find('.status-success').removeClass('d-none'); elem.find('.status-success').removeClass('d-none');
elem.removeClass('client'); elem.removeClass('client');
...@@ -68,22 +60,20 @@ function logout_services() { ...@@ -68,22 +60,20 @@ function logout_services() {
.catch(function (err) { .catch(function (err) {
elem.find('.status-active').addClass('d-none'); elem.find('.status-active').addClass('d-none');
elem.find('.status-failed').removeClass('d-none'); elem.find('.status-failed').removeClass('d-none');
console.log(err);
throw err; throw err;
}); });
all_promises.push(p); all_promises.push(p);
}); });
Promise.allSettled(all_promises).then(function (results) { Promise.allSettled(all_promises).then(function (results) {
console.log(results);
for (result of results) { for (result of results) {
if (result.status == 'rejected') if (result.status == 'rejected')
throw result.reason; throw result.reason;
} }
$('#cont-text').text('Done, redirecting ...'); $('#cont-text').text({{_('Done, redirecting ...')|tojson}});
window.location = {{ (request.values.get('ref') or '/')|tojson }}; window.location = {{ (request.values.get('ref') or '/')|tojson }};
}).catch(function (err) { }).catch(function (err) {
$("#retry-button").prop('disabled', false); $("#retry-button").prop('disabled', false);
$('#cont-text').text('Log out failed on some services. Retry?'); $('#cont-text').text({{_('Log out failed on some services. Retry?')|tojson}});
}); });
} }
......
...@@ -3,40 +3,29 @@ ...@@ -3,40 +3,29 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table class="table table-striped"> <p class="text-right">
<a class="btn btn-primary" href="{{ url_for("role.new") }}">
<i class="fa fa-plus" aria-hidden="true"></i> {{_("New")}}
</a>
</p>
<table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">id</th> <th scope="col">{{_("Name")}}</th>
<th scope="col">name</th> <th scope="col">{{_("Description")}}</th>
<th scope="col">description</th>
<th scope="col">
<p class="text-right">
<a class="btn btn-primary" href="{{ url_for("role.show") }}">
<i class="fa fa-plus" aria-hidden="true"></i> New
</a>
</p>
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for role in roles|sort(attribute="name") %} {% for role in roles|sort(attribute="name") %}
<tr id="role-{{ role.id }}"> <tr id="role-{{ role.id }}">
<td>
{{ role.id }}
</td>
<th scope="row"> <th scope="row">
{{ role.name }} <a href="{{ url_for("role.show", roleid=role.id) }}">
{{ role.name or _('<empty name>') }}
</a>
</th> </th>
<td> <td>
{{ role.description }} {{ role.description }}
</td> </td>
<td>
<p class="text-right">
<a href="{{ url_for("role.show", roleid=role.id) }}" class="btn btn-primary">
<i class="fa fa-edit" aria-hidden="true"></i> Edit
</a>
</p>
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
......
{% extends 'base.html' %}
{% block body %}
{% if role.locked %}
<div class="alert alert-warning" role="alert">
{{_("Name, moderator group, included roles and groups of this role are managed externally.")}} <a href="{{ url_for("role.unlock", roleid=role.id) }}" class="alert-link">Unlock this role</a> to edit them at the risk of having your changes overwritten.
</div>
{% endif %}
<form action="{{ url_for("role.update", roleid=role.id) }}" method="POST" autocomplete="off">
<div class="align-self-center">
<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("role.index") }}" class="btn btn-secondary">{{_("Cancel")}}</a>
{% if role.id %}
{% if not role.is_default %}
<a href="{{ url_for("role.set_default", roleid=role.id) }}" onClick='return confirm({{_("All non-service users will be removed as members from this role and get its permissions implicitly. Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Set as default")}}</a>
{% else %}
<a href="{{ url_for("role.unset_default", roleid=role.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-secondary">{{_("Unset as default")}}</a>
{% endif %}
<a href="{{ url_for("role.delete", roleid=role.id) }}" onClick='return confirm({{_("Are you sure?")|tojson}});' class="btn btn-danger {{ 'disabled' if role.locked }}"><i class="fa fa-trash" aria-hidden="true"></i> {{_("Delete")}}</a>
{% else %}
<a href="#" class="btn btn-secondary disabled">{{_("Set as default")}}</a>
<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="settings-tab" data-toggle="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="true">{{_("Settings")}}</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">{{_("Included roles")}} <span class="badge badge-pill badge-secondary">{{ role.included_roles|length }}</span></a>
</li>
<li class="nav-item">
<a class="nav-link" id="groups-tab" data-toggle="tab" href="#groups" role="tab" aria-controls="groups" aria-selected="false">{{_("Included groups")}} <span class="badge badge-pill badge-secondary">{{ role.groups|length }}</span></a>
</li>
</ul>
<div class="tab-content border mb-2 pt-2" id="tabcontent">
<div class="tab-pane fade show active" id="settings" role="tabpanel" aria-labelledby="settings-tab">
<div class="form-group col">
<label for="role-name">{{_("Role Name")}}</label>
<input type="text" class="form-control" id="role-name" name="name" value="{{ role.name or '' }}" {{ 'disabled' if role.locked }}>
<small class="form-text text-muted">
</small>
</div>
<div class="form-group col">
<label for="role-description">{{_("Description")}}</label>
<textarea class="form-control" id="role-description" name="description" rows="5">{{ role.description or '' }}</textarea>
<small class="form-text text-muted">
</small>
</div>
<div class="form-group col">
<label for="moderator-group">{{_("Moderator Group")}}</label>
<select class="form-control" id="moderator-group" name="moderator-group" {{ 'disabled' if role.locked }}>
<option value="" class="text-muted">{{_("No Moderator Group")}}</option>
{% for group in groups %}
<option value="{{ group.id }}" {{ 'selected' if group == role.moderator_group }}>{{ group.name }}</option>
{% endfor %}
</select>
</div>
<div class="form-group col">
<span>{{_("Moderators")}}:</span>
<ul class="row">
{% for moderator in role.moderator_group.members %}
<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=moderator.id) }}">{{ moderator.loginname }}</a></li>
{% endfor %}
</ul>
</div>
<div class="form-group col">
<span>{{_("Members")}}:</span>
<ul class="row">
{% for member in role.members|sort(attribute='loginname') %}
<li class="col-12 col-xs-6 col-sm-4 col-md-3 col-lg-2"><a href="{{ url_for("user.show", id=member.id) }}">{{ member.loginname }}</a></li>
{% endfor %}
</ul>
</div>
</div>
<div class="tab-pane fade" id="roles" role="tabpanel" aria-labelledby="roles-tab">
<div class="form-group col">
<span>{{_("Roles to include groups from recursively")}}</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{_("name")}}</th>
<th scope="col">{{_("description")}}</th>
<th scope="col">{{_("currently includes groups")}}</th>
</tr>
</thead>
<tbody>
{% for r in roles|sort(attribute="name")|sort(attribute='name') %}
<tr id="include-role-{{ r.id }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="include-role-{{ r.id }}-checkbox" name="include-role-{{ r.id }}" value="1" aria-label="enabled"
{% if r == role or role.locked %} disabled{% endif %}
{% if r in role.included_roles %} checked{% endif %}>
</div>
</td>
<td>
<a href="{{ url_for("role.show", roleid=r.id) }}">
{{ r.name }}
</a>
</td>
<td>
{{ r.description }}
</td>
<td>
{% for group in r.groups_effective|sort(attribute='name') %}
<a href="{{ url_for("group.show", id=group.id) }}">{{ group.name }}</a>{{ ', ' if not loop.last }}
{% endfor %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="tab-pane fade" id="groups" role="tabpanel" aria-labelledby="groups-tab">
<div class="form-group col">
<span>{{_("Included groups")}}</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">{{_("name")}}</th>
<th scope="col">{{_("description")}}</th>
<th scope="col">{{_("2FA required")}}</th>
</tr>
</thead>
<tbody>
{% for group in groups|sort(attribute="name") %}
<tr id="group-{{ group.id }}">
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="group-{{ group.id }}-checkbox" name="group-{{ group.id }}" value="1" aria-label="enabled" {% if group in role.groups %}checked{% endif %} {{ 'disabled' if role.locked }}>
</div>
</td>
<td>
<a href="{{ url_for("group.show", id=group.id) }}">
{{ group.name }}
</a>
</td>
<td>
{{ group.description }}
</td>
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="group-mfa-{{ group.id }}-checkbox" name="group-mfa-{{ group.id }}" value="1" aria-label="enabled" {% if group in role.groups and role.groups[group].requires_mfa %}checked{% endif %} {{ 'disabled' if role.locked }}>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</form>
{% endblock %}
...@@ -3,27 +3,23 @@ ...@@ -3,27 +3,23 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<table class="table table-striped"> <table class="table table-striped table-sm">
<thead> <thead>
<tr> <tr>
<th scope="col">gid</th> <th scope="col">{{_('Name')}}</th>
<th scope="col">name</th> <th scope="col">{{_('Description')}}</th>
<th scope="col">description</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for group in groups|sort(attribute="gid") %} {% for role in roles|sort(attribute='name') %}
<tr id="group-{{ group.gid }}"> <tr id="role-{{ role.id }}">
<th scope="row"> <th scope="row">
{{ group.gid }} <a href="{{ url_for('rolemod.show', role_id=role.id) }}">
</th> {{ role.name or '<empty name>' }}
<td>
<a href="{{ url_for("group.show", gid=group.gid) }}">
{{ group.name }}
</a> </a>
</td> </th>
<td> <td>
{{ group.description }} {{ role.description }}
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
......
{% extends 'base.html' %}
{% block body %}
<form method="POST" action="{{ url_for("rolemod.update", role_id=role.id) }}">
<div class="float-sm-right pb-2">
<a href="{{ url_for("invite.new", **{"role-%d"%role.id: 1}) }}" class="btn btn-primary mr-2"><i class="fa fa-link" aria-hidden="true"></i> {{_('Invite Members')}}</a>
<button type="submit" class="btn btn-primary"><i class="fa fa-save" aria-hidden="true"></i> {{_('Save')}}</button>
<a href="{{ url_for("rolemod.index") }}" class="btn btn-secondary">{{_('Cancel')}}</a>
</div>
<ul class="nav nav-tabs pt-2 border-0" id="tablist" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="overview-tab" data-toggle="tab" href="#overview" role="tab" aria-controls="overview" aria-selected="true">{{_('Overview')}}</a>
</li>
<li class="nav-item">
<a class="nav-link" id="members-tab" data-toggle="tab" href="#members" role="tab" aria-controls="members" aria-selected="false">{{_('Members')}} <span class="badge badge-pill badge-secondary">{{ role.members|length }}</span></a>
</li>
</ul>
<div class="tab-content border mb-2 pt-2" id="tabcontent">
<div class="tab-pane fade show active" id="overview" role="tabpanel" aria-labelledby="overview-tab">
<div class="form-group col">
<label for="role-name">{{_('Role name')}}</label>
<input type="text" class="form-control" id="role-name" value="{{ role.name }}" readonly>
</div>
<div class="form-group col">
<label for="role-description">{{_('Description')}}</label>
<textarea class="form-control" id="role-description" rows="5" name="description">{{ role.description }}</textarea>
</div>
<div class="form-group col">
<label>{{_('Moderators:')}}</label>
<ul>
{% for moderator in role.moderator_group.members %}
<li>{{ moderator.displayname }} ({{ moderator.loginname }})</li>
{% endfor %}
</ul>
</div>
</div>
<div class="tab-pane fade" id="members" role="tabpanel" aria-labelledby="members-tab">
<div class="col">
<span>{{_('Role members:')}}</span>
<table class="table table-striped table-sm">
<thead>
<tr>
<th scope="col">{{_('Name')}}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for member in role.members|sort(attribute="loginname") %}
<tr>
<td>{{ member.displayname }} ({{ member.loginname }})</td>
<td class="text-right">
<a class="btn btn-danger py-0" href="{{ url_for('rolemod.delete_member', role_id=role.id, member_id=member.id) }}">{{_('Remove')}}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</form>
{% endblock %}
{% extends 'base.html' %}
{% block body %}
<p>
{{_("When you proceed, all recovery codes, registered authenticator applications and devices will be invalidated.
You can later generate new recovery codes and setup your applications and devices again.")}}
</p>
<form class="form" action="{{ url_for('selfservice.disable_mfa_confirm') }}" method="POST">
<button type="submit" class="btn btn-danger btn-block">{{_("Disable two-factor authentication")}}</button>
</form>
{% endblock %}
{% extends 'base_narrow.html' %}
{% block body %}
<form action="{{ url_for("selfservice.forgot_password") }}" method="POST">
<div class="col-12">
<h2 class="text-center">{{_("Forgot password")}}</h2>
</div>
<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">
</div>
<div class="form-group col-12">
<label for="user-mail">{{_("Mail Address")}}</label>
<input type="email" autocomplete="email" class="form-control" id="user-mail" name="mail" required="required" tabindex="2">
</div>
<div class="form-group col-12">
<button type="submit" class="btn btn-primary btn-block" tabindex="3">{{_("Send password reset mail")}}</button>
</div>
</form>
{% endblock %}
Hi {{ user.displayname }}, Hi {{ user.displayname }},
you have requested to change your mail address. To confirm the change, please visit the following url: you have requested to change your mail address. To confirm the change, please visit the following url:
{{ url_for('selfservice.token_mail', token=token, _external=True) }} {{ url_for('selfservice.verify_email', email_id=email.id, secret=secret, _external=True) }}
**The link is valid for 48h** **The link is valid for 48h**
Hi {{ user.displayname }},
welcome to the {{ config.ORGANISATION_NAME }} infrastructure! An account was created for you.
Please visit the following url to set your password:
{{ url_for('selfservice.token_password', token_id=token.id, token=token.token, _external=True) }}
**The link is valid for 48h**
{% if config.WELCOME_TEXT %}
{{ config.WELCOME_TEXT }}
{% endif -%}
If you think the account was created by mistake, please contact the administrators at {{ config.ORGANISATION_CONTACT }}.
Hi {{ user.displayname }}, Hi {{ user.displayname }},
you have requested a password reset. To reset your password, visit the following url: you have requested a password reset. To reset your password, visit the following url:
{{ url_for('selfservice.token_password', token=token, _external=True) }} {{ url_for('selfservice.token_password', token_id=token.id, token=token.token, _external=True) }}
**The link is valid for 48h** **The link is valid for 48h**
......
{% extends 'base.html' %}
{% block body %}
{% if not user.mfa_enabled and user.compute_groups() != user.compute_groups(ignore_mfa=True) %}
<div class="alert alert-warning" role="alert">
{{_("Some permissions require you to setup two-factor authentication.
These permissions are not in effect until you do that.")}}
</div>
{% endif %}
<div class="row">
<div class="col-12 col-md-5">
<h5>{{_("Profile")}}</h5>
<p>{{_("Your profile information is used by all services that are integrated into the Single-Sign-On.")}}</p>
<p>{{_("Changes may take several minutes to be visible in all services.")}}</p>
</div>
<div class="col-12 col-md-7">
<form class="form" action="{{ url_for("selfservice.update_profile") }}" method="POST">
<div class="form-group">
<label>{{_("Login Name")}}</label>
<input type="text" class="form-control" value="{{ user.loginname }}" readonly>
</div>
<div class="form-group">
<label>{{_("Display Name")}}</label>
<input type="text" class="form-control" id="user-displayname" name="displayname" value="{{ user.displayname }}">
</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">
<label>{{_("Primary 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">
<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>
</div>
</div>
<hr>
<div class="row mt-3">
<div class="col-12 col-md-5">
<h5>{{_("Password")}}</h5>
<p>{{_("Your login password for the Single-Sign-On. Only enter it on the Single-Sign-On login page! No other legit websites will ask you for this password. We do not ever need your password to assist you.")}}</p>
</div>
<div class="col-12 col-md-7">
<form class="form" action="{{ url_for("selfservice.change_password") }}" method="POST">
<div class="form-group">
<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">
{{ User.PASSWORD_DESCRIPTION|safe }}
</small>
</div>
<div class="form-group">
<input type="password" autocomplete="new-password" class="form-control" id="user-password2" name="password2" placeholder="{{_("Repeat Password")}}" required>
</div>
<button type="submit" class="btn btn-primary btn-block">{{_("Change Password")}}</button>
</form>
</div>
</div>
<hr>
<div class="row mt-3">
<div class="col-12 col-md-5">
<h5>{{_("Two-Factor Authentication")}}</h5>
<p>{{_("Setting up Two-Factor Authentication (2FA) adds an additional step to the Single-Sign-On login and increases the security of your account significantly.")}}</p>
</div>
<div class="col-12 col-md-7">
<p>
{% if user.mfa_enabled %}
{{ _("Two-factor authentication is currently <strong>enabled</strong>.")|safe }}
{% else %}
{{ _("Two-factor authentication is currently <strong>disabled</strong>.")|safe }}
{% endif %}
</p>
<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>
<hr>
<div class="row mt-3">
<div class="col-12 col-md-5">
<h5>{{_("Roles")}}</h5>
<p>{{_("Aside from a set of base permissions, your roles determine the permissions of your account.")}}</p>
{% if config['SERVICES'] %}
<p>{{_("See <a href=\"%(services_url)s\">Services</a> for an overview of your current permissions.", services_url=url_for('service.overview'))}}</p>
{% endif %}
</div>
<div class="col-12 col-md-7">
<p>{{_("Administrators and role moderators can invite you to new roles.")}}</p>
<table class="table">
<thead>
<tr>
<th scope="col">{{_("Name")}}</th>
<th scope="col">{{_("Description")}}</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for role in user.roles|sort(attribute='name') %}
<tr>
<td>{{ role.name }}
{% if not user.mfa_enabled and role.groups.values()|selectattr('requires_mfa')|list %}
<i class="fas fa-exclamation-triangle text-warning" title="{{_("Some permissions in this role require you to setup two-factor authentication")}}"></i>
{% endif %}
</td>
<td>{{ role.description }}</td>
<td>
<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>
</form>
</td>
</tr>
{% endfor %}
{% if not user.roles %}
<tr class="table-secondary">
<td colspan=3 class="text-center">{{_("You currently don't have any roles")}}</td>
</tr>
{% endif %}
</tbody>
</table>
</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 %}
{% 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 (TOTP)</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,27 +131,34 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -114,27 +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 %} {% if not webauthn_supported %}
<div class="alert alert-warning" role="alert">U2F/FIDO2 support not enabled</div> <div class="alert alert-warning" role="alert">{{_("U2F/FIDO2 support not enabled")}}</div>
{% endif %} {% 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>
...@@ -142,22 +166,22 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -142,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>
...@@ -172,21 +196,21 @@ You need to setup at least one authentication method to enable two-factor authen ...@@ -172,21 +196,21 @@ You need to setup at least one authentication method to enable two-factor authen
$('#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({
...@@ -198,34 +222,34 @@ $('#webauthn-form').on('submit', function(e) { ...@@ -198,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;
...@@ -237,7 +261,7 @@ if (typeof(PublicKeyCredential) != "undefined") { ...@@ -237,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');
} }
......
...@@ -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 %}