140 lines
No EOL
7.5 KiB
HTML
140 lines
No EOL
7.5 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ action }} OAuth Client - Mock API Admin{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="content-header">
|
||
<h1><i class="bi bi-pencil-square"></i> {{ action }} OAuth Client</h1>
|
||
<p class="lead">Configure an OAuth 2.0 client registration.</p>
|
||
</div>
|
||
|
||
{% if error %}
|
||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||
{{ error }}
|
||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="row">
|
||
<div class="col-lg-8">
|
||
<div class="card">
|
||
<div class="card-body">
|
||
<form method="post" action="{{ form_action }}" id="client-form">
|
||
{% if client and client.id %}
|
||
<input type="hidden" name="id" value="{{ client.id }}">
|
||
{% endif %}
|
||
|
||
<div class="mb-3">
|
||
<label for="client_name" class="form-label">Client Name <span class="text-danger">*</span></label>
|
||
<input type="text" class="form-control {% if errors and errors.client_name %}is-invalid{% endif %}" id="client_name" name="client_name" value="{{ client.name if client else '' }}" placeholder="My API Client" required>
|
||
<div class="invalid-feedback">
|
||
{{ errors.client_name if errors and errors.client_name else 'Client name is required.' }}
|
||
</div>
|
||
<div class="form-text">
|
||
Human-readable name for this client.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="redirect_uris" class="form-label">Redirect URIs <span class="text-danger">*</span></label>
|
||
<textarea class="form-control {% if errors and errors.redirect_uris %}is-invalid{% endif %}" id="redirect_uris" name="redirect_uris" rows="3" required>{{ client.redirect_uris | join(', ') if client else '' }}</textarea>
|
||
<div class="invalid-feedback">
|
||
{{ errors.redirect_uris if errors and errors.redirect_uris else 'Enter one or more redirect URIs separated by commas.' }}
|
||
</div>
|
||
<div class="form-text">
|
||
Comma-separated list of allowed redirect URIs (must be http:// or https://). Example: <code>https://myapp.com/callback, https://localhost:3000/callback</code>.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="grant_types" class="form-label">Grant Types <span class="text-danger">*</span></label>
|
||
<textarea class="form-control {% if errors and errors.grant_types %}is-invalid{% endif %}" id="grant_types" name="grant_types" rows="2" required>{{ client.grant_types | join(', ') if client else 'authorization_code,client_credentials,refresh_token' }}</textarea>
|
||
<div class="invalid-feedback">
|
||
{{ errors.grant_types if errors and errors.grant_types else 'Enter allowed grant types separated by commas.' }}
|
||
</div>
|
||
<div class="form-text">
|
||
Comma-separated list of OAuth 2.0 grant types. Allowed values: <code>authorization_code</code>, <code>client_credentials</code>, <code>password</code>, <code>refresh_token</code>.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label for="scopes" class="form-label">Scopes</label>
|
||
<textarea class="form-control {% if errors and errors.scopes %}is-invalid{% endif %}" id="scopes" name="scopes" rows="2">{{ client.scopes | join(', ') if client else 'read,write' }}</textarea>
|
||
<div class="invalid-feedback">
|
||
{{ errors.scopes if errors and errors.scopes else 'Enter allowed scopes separated by commas.' }}
|
||
</div>
|
||
<div class="form-text">
|
||
Comma-separated list of OAuth scopes that this client can request. Example: <code>read,write,admin</code>.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" role="switch" id="is_active" name="is_active" {% if client and client.is_active %}checked{% endif %}>
|
||
<label class="form-check-label" for="is_active">Client is active</label>
|
||
</div>
|
||
<div class="form-text">
|
||
Inactive clients cannot authenticate or obtain tokens.
|
||
</div>
|
||
</div>
|
||
|
||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||
<a href="/admin/oauth/clients" class="btn btn-outline-secondary me-md-2">Cancel</a>
|
||
<button type="submit" class="btn btn-primary">Save Client</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="card">
|
||
<div class="card-header">
|
||
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Help</h5>
|
||
</div>
|
||
<div class="card-body">
|
||
<h6>Client Credentials</h6>
|
||
<p>Client ID and secret will be generated automatically upon creation. The secret will be shown only once – store it securely.</p>
|
||
|
||
<h6>Redirect URIs</h6>
|
||
<p>Must be absolute URIs with scheme http:// or https://. The redirect URI used in authorization requests must match exactly.</p>
|
||
|
||
<h6>Grant Types</h6>
|
||
<ul class="small">
|
||
<li><strong>authorization_code</strong>: For web server applications.</li>
|
||
<li><strong>client_credentials</strong>: For machine‑to‑machine authentication.</li>
|
||
<li><strong>password</strong>: For trusted first‑party clients (discouraged).</li>
|
||
<li><strong>refresh_token</strong>: Allows obtaining new access tokens.</li>
|
||
</ul>
|
||
|
||
<h6>Security</h6>
|
||
<p>Client secrets are hashed using bcrypt before storage. Never expose secrets in logs or client‑side code.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block extra_scripts %}
|
||
<script>
|
||
// Simple validation for comma-separated lists
|
||
function validateList(textareaId) {
|
||
const textarea = document.getElementById(textareaId);
|
||
if (textarea.value.trim() === '') {
|
||
textarea.classList.add('is-invalid');
|
||
return false;
|
||
}
|
||
textarea.classList.remove('is-invalid');
|
||
return true;
|
||
}
|
||
|
||
document.getElementById('redirect_uris')?.addEventListener('blur', () => validateList('redirect_uris'));
|
||
document.getElementById('grant_types')?.addEventListener('blur', () => validateList('grant_types'));
|
||
|
||
document.getElementById('client-form')?.addEventListener('submit', function(e) {
|
||
let valid = true;
|
||
if (!validateList('redirect_uris')) valid = false;
|
||
if (!validateList('grant_types')) valid = false;
|
||
if (!valid) e.preventDefault();
|
||
});
|
||
</script>
|
||
{% endblock %} |