[Web] Rework API keys: multiple read-only/read-write keys, service keys from mailcow.conf

This commit is contained in:
FreddleSpl0it committed 2026-10-23 11:20:17 +02:00
1 parent c145f858e2
commit 0dadf49934
21 files changed
+942 -335

No files matched your search

+60 -135
View File
@@ -87,141 +87,6 @@
</div>
<br>
</div>
<legend style="cursor:pointer;margin-top:20px" data-bs-target="#admin_api" unselectable="on" data-bs-toggle="collapse">
<i style="font-size:10pt;" class="bi bi-plus-square"></i> API
</legend>
<hr />
<div id="admin_api" class="collapse">
<div class="row">
<div class="col-lg-12">
<p class="text-muted">{{ lang.admin.api_info|raw }}</p>
</div>
<div class="col-lg-12">
<div class="card mb-3">
<div class="card-header">
<h4 class="card-title"><i class="bi bi-file-earmark-arrow-down"></i> {{ lang.admin.cors_settings }}</h4>
</div>
<div class="card-body">
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" data-id="editcors" method="post">
<div class="row mb-4">
<label class="control-label col-sm-2 mb-4" for="allowed_origins">{{ lang.admin.allowed_origins }}</label>
<div class="col-sm-9 mb-4">
<textarea class="form-control textarea-code" rows="7" name="allowed_origins" id="allowed_origins">{{ cors_settings.allowed_origins }}</textarea>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="allowed_methods">{{ lang.admin.allowed_methods }}</label>
<div class="col-sm-9">
<select name="allowed_methods" id="allowed_methods" multiple class="form-control">
<option value="POST"{% if "POST" in cors_settings.allowed_methods %} selected{% endif %}>POST</option>
<option value="GET"{% if "GET" in cors_settings.allowed_methods %} selected{% endif %}>GET</option>
<option value="DELETE"{% if "DELETE" in cors_settings.allowed_methods %} selected{% endif %}>DELETE</option>
<option value="PUT"{% if "PUT" in cors_settings.allowed_methods %} selected{% endif %}>PUT</option>
</select>
</div>
</div>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-9 d-grid d-sm-block">
<button class="btn btn-sm btn-xs-lg btn-success" data-item="cors" data-api-url="edit/cors" data-id="editcors" data-action="edit_selected" href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card mb-3">
<div class="card-header">
<h4 class="card-title"><i class="bi bi-file-earmark-arrow-down"></i> {{ lang.admin.api_read_only }}</h4>
</div>
<div class="card-body">
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
<div class="row mb-4">
<label class="control-label col-sm-3" for="allow_from_ro">{{ lang.admin.api_allow_from }}:</label>
<div class="col-sm-9">
<textarea class="form-control textarea-code" rows="7" name="allow_from" id="allow_from_ro" {% if api.ro.skip_ip_check %}disabled{% endif %} required>{{ api.ro.allow_from }}</textarea>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<label>
<input type="checkbox" class="form-check-input" name="skip_ip_check" id="skip_ip_check_ro" {% if api.ro.skip_ip_check %}checked{% endif %}> {{ lang.admin.api_skip_ip_check }}
</label>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-3">{{ lang.admin.api_key }}:</label>
<div class="col-sm-9">
<input type="text" class="form-control" value="{{ api.ro.api_key|default('-') }}" readonly />
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<label>
<input type="checkbox" class="form-check-input" name="active" {% if api.ro.active %}checked{% endif %}> {{ lang.admin.activate_api }}
</label>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<div class="btn-group">
<button class="btn btn-sm btn-xs-lg btn-xs-half d-block d-sm-inline btn-success" name="admin_api[ro]" type="submit" href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
<button class="btn btn-sm btn-xs-lg btn-xs-half d-block d-sm-inline btn-secondary admin-ays-dialog" name="admin_api_regen_key[ro]" type="submit" href="#" {% if not api.ro.api_key %}disabled{% endif %}>{{ lang.admin.regen_api_key }}</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card mb-3">
<div class="card-header">
<h4 class="card-title"><i class="bi bi-file-earmark-diff"></i> {{ lang.admin.api_read_write }}</h4>
</div>
<div class="card-body">
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" method="post">
<div class="row mb-4">
<label class="control-label col-sm-3" for="allow_from_rw">{{ lang.admin.api_allow_from }}:</label>
<div class="col-sm-9">
<textarea class="form-control textarea-code" rows="7" name="allow_from" id="allow_from_rw" {% if api.rw.skip_ip_check %}disabled{% endif %} required>{{ api.rw.allow_from }}</textarea>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<label>
<input type="checkbox" class="form-check-input" name="skip_ip_check" id="skip_ip_check_rw" {% if api.rw.skip_ip_check %}checked{% endif %}> {{ lang.admin.api_skip_ip_check }}
</label>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-3" for="admin_api_key">{{ lang.admin.api_key }}:</label>
<div class="col-sm-9">
<input type="text" class="form-control" value="{{ api.rw.api_key|default('-') }}" readonly />
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<label>
<input type="checkbox" class="form-check-input" name="active" {% if api.rw.active %}checked{% endif %}> {{ lang.admin.activate_api }}
</label>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-3 col-sm-9">
<div class="btn-group">
<button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-success" name="admin_api[rw]" type="submit" href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
<button class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary admin-ays-dialog" name="admin_api_regen_key[rw]" type="submit" {% if not api.rw.api_key %}disabled{% endif %} href="#">{{ lang.admin.regen_api_key }}</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -251,4 +116,64 @@
</div>
</div>
</div>
<div class="card mb-4">
<div class="card-header d-flex fs-5">
<button class="btn d-md-none flex-grow-1 text-start" data-bs-target="#collapse-tab-config-api" data-bs-toggle="collapse" aria-controls="collapse-tab-config-api">
API
</button>
<span class="d-none d-md-block">API</span>
</div>
<div id="collapse-tab-config-api" class="card-body collapse" data-bs-parent="#admin-content">
<p class="text-muted">{{ lang.admin.api_info|raw }}</p>
{# API keys #}
<legend style="margin-top:20px"><i class="bi bi-key"></i> {{ lang.admin.api_keys }}</legend>
<hr />
<table id="apikeystable" class="table table-striped dt-responsive w-100"></table>
<div class="mass-actions-admin mb-4">
<div class="btn-group">
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary" id="toggle_multi_select_all" data-id="api_keys" href="#"><i class="bi bi-check-all"></i> {{ lang.mailbox.toggle_all }}</a>
<a class="btn btn-sm btn-xs-half d-block d-sm-inline btn-secondary dropdown-toggle" data-bs-toggle="dropdown" href="#">{{ lang.mailbox.quick_actions }}</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" data-action="edit_selected" data-id="api_keys" data-api-url='edit/api-key' data-api-attr='{"active":"1"}' href="#">{{ lang.mailbox.activate }}</a></li>
<li><a class="dropdown-item" data-action="edit_selected" data-id="api_keys" data-api-url='edit/api-key' data-api-attr='{"active":"0"}' href="#">{{ lang.mailbox.deactivate }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item admin-ays-dialog" data-action="edit_selected" data-id="api_keys" data-api-url='edit/api-key' data-api-attr='{"regen_key":"1"}' href="#">{{ lang.admin.regen_api_key }}</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" data-action="delete_selected" data-id="api_keys" data-api-url='delete/api-key' href="#">{{ lang.mailbox.remove }}</a></li>
</ul>
<a class="btn btn-sm d-block d-sm-inline btn-success" data-id="add_api_key" data-bs-toggle="modal" data-bs-target="#addApiKeyModal" href="#"><i class="bi bi-plus-lg"></i> {{ lang.admin.api_key_add }}</a>
</div>
</div>
{# CORS #}
<legend style="margin-top:20px"><i class="bi bi-file-earmark-arrow-down"></i> {{ lang.admin.cors_settings }}</legend>
<hr />
<form class="form-horizontal" autocapitalize="none" autocorrect="off" role="form" data-id="editcors" method="post">
<div class="row mb-4">
<label class="control-label col-sm-2" for="allowed_origins">{{ lang.admin.allowed_origins }}</label>
<div class="col-sm-9">
<textarea class="form-control textarea-code" rows="7" name="allowed_origins" id="allowed_origins">{{ cors_settings.allowed_origins }}</textarea>
</div>
</div>
<div class="row mb-4">
<label class="control-label col-sm-2" for="allowed_methods">{{ lang.admin.allowed_methods }}</label>
<div class="col-sm-9">
<select name="allowed_methods" id="allowed_methods" multiple class="form-control">
<option value="POST"{% if "POST" in cors_settings.allowed_methods %} selected{% endif %}>POST</option>
<option value="GET"{% if "GET" in cors_settings.allowed_methods %} selected{% endif %}>GET</option>
<option value="DELETE"{% if "DELETE" in cors_settings.allowed_methods %} selected{% endif %}>DELETE</option>
<option value="PUT"{% if "PUT" in cors_settings.allowed_methods %} selected{% endif %}>PUT</option>
</select>
</div>
</div>
<div class="row">
<div class="offset-sm-2 col-sm-9 d-grid d-sm-block">
<button class="btn btn-sm btn-xs-lg btn-success" data-item="cors" data-api-url="edit/cors" data-id="editcors" data-action="edit_selected" href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
+71
View File
@@ -0,0 +1,71 @@
{% extends 'edit.twig' %}
{% block inner_content %}
{% if result %}
{% set locked = result.service == 1 %}
<h4>{{ lang.admin.api_key }}</h4>
{% if locked %}
<div class="alert alert-secondary" role="alert"><i class="bi bi-lock-fill"></i> {{ lang.admin.api_service_key_info }}</div>
{% endif %}
<form data-id="edit_api_key" class="form-horizontal" role="form" method="post" autocapitalize="none" autocorrect="off">
<input type="hidden" value="0" name="skip_ip_check">
<input type="hidden" value="0" name="active">
<div class="row mb-2">
<label class="control-label col-sm-2" for="api_key">{{ lang.admin.api_key }}</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" class="form-control textarea-code" id="api_key" value="{{ result.api_key }}" readonly>
<button type="button" class="btn btn-secondary api-key-copy" data-key="{{ result.api_key }}"><i class="bi bi-clipboard"></i></button>
</div>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="description">{{ lang.admin.description }}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="description" id="description" value="{{ result.description }}" maxlength="255"{% if locked %} disabled{% endif %}>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="access">{{ lang.admin.access }}</label>
<div class="col-sm-10">
<select class="form-control" name="access" id="access"{% if locked %} disabled{% endif %}>
<option value="ro"{% if result.access == 'ro' %} selected{% endif %}>{{ lang.admin.api_read_only }}</option>
<option value="rw"{% if result.access == 'rw' %} selected{% endif %}>{{ lang.admin.api_read_write }}</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2" for="allow_from">{{ lang.admin.api_allow_from }}</label>
<div class="col-sm-10">
<textarea class="form-control textarea-code" rows="5" name="allow_from" id="allow_from"{% if locked or result.skip_ip_check == 1 %} disabled{% endif %}>{{ result.allow_from|join("\n") }}</textarea>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input api-skip-ip-check" value="1" name="skip_ip_check" data-target="#allow_from"{% if result.skip_ip_check == 1 %} checked{% endif %}{% if locked %} disabled{% endif %}> {{ lang.admin.api_skip_ip_check }}</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="active"{% if result.active == 1 %} checked{% endif %}{% if locked %} disabled{% endif %}> {{ lang.edit.active }}</label>
</div>
</div>
</div>
{% if not locked %}
<div class="row">
<div class="offset-sm-2 col-sm-10">
<div class="btn-group">
<button class="btn btn-xs-lg btn-xs-half d-block d-sm-inline btn-success" data-action="edit_selected" data-id="edit_api_key" data-item="{{ result.id }}" data-api-url='edit/api-key' data-api-attr='{}' href="#"><i class="bi bi-check-lg"></i> {{ lang.admin.save }}</button>
<button class="btn btn-xs-lg btn-xs-half d-block d-sm-inline btn-secondary api-key-regen" data-action="edit_selected" data-id="regen_api_key" data-item="{{ result.id }}" data-api-url='edit/api-key' data-api-attr='{"regen_key":"1"}' href="#"><i class="bi bi-arrow-repeat"></i> {{ lang.admin.regen_api_key }}</button>
</div>
</div>
</div>
{% endif %}
</form>
{% else %}
{{ parent() }}
{% endif %}
{% endblock %}
+57
View File
@@ -150,6 +150,63 @@
</div>
</div>
</div><!-- add domain admin modal -->
<!-- add api key modal -->
<div class="modal fade" id="addApiKeyModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">{{ lang.admin.api_key_add }}</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form class="form-horizontal" data-id="add_api_key" role="form" method="post" autocapitalize="none" autocorrect="off">
<input type="hidden" value="0" name="skip_ip_check">
<input type="hidden" value="0" name="active">
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="add_api_description">{{ lang.admin.description }}:</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="description" id="add_api_description" maxlength="255">
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="add_api_access">{{ lang.admin.access }}:</label>
<div class="col-sm-10">
<select class="form-control" name="access" id="add_api_access">
<option value="ro">{{ lang.admin.api_read_only }}</option>
<option value="rw">{{ lang.admin.api_read_write }}</option>
</select>
</div>
</div>
<div class="row mb-2">
<label class="control-label col-sm-2 text-sm-end" for="add_api_allow_from">{{ lang.admin.api_allow_from }}:</label>
<div class="col-sm-10">
<textarea class="form-control textarea-code" rows="5" name="allow_from" id="add_api_allow_from" required></textarea>
</div>
</div>
<div class="row mb-2">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input api-skip-ip-check" value="1" name="skip_ip_check" data-target="#add_api_allow_from"> {{ lang.admin.api_skip_ip_check }}</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<div class="form-check">
<label><input type="checkbox" class="form-check-input" value="1" name="active" checked> {{ lang.admin.active }}</label>
</div>
</div>
</div>
<div class="row mb-4">
<div class="offset-sm-2 col-sm-10">
<button class="btn btn-xs-lg d-block d-sm-inline btn-success" data-action="add_item" data-id="add_api_key" data-api-url='add/api-key' data-api-attr='{}' href="#"><i class="bi bi-plus-lg"></i> {{ lang.admin.add }}</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div><!-- add api key modal -->
<!-- add admin modal -->
<div class="modal fade" id="addAdminModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-xl">