Files
mailova/app/templates/api_docs.html

322 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}{{ 'api_docs.title'|t(user.language) }} - {{ 'brand'|t(user.language) }}{% endblock %}
{% block content %}
<style>
.doc-section { margin-bottom:2rem; }
.doc-section h2 { font-size:1.15rem; margin-bottom:.75rem; padding-bottom:.5rem; border-bottom:1px solid var(--border); }
.doc-section h3 { font-size:1rem; margin:1rem 0 .5rem; }
.doc-endpoint { background:var(--card-bg); border:1px solid var(--border); border-radius:8px; padding:1rem 1.25rem; margin-bottom:1rem; }
.doc-endpoint .ep-header { display:flex; align-items:center; gap:.6rem; margin-bottom:.5rem; flex-wrap:wrap; }
.doc-method { display:inline-block; padding:2px 8px; border-radius:4px; font-size:.75rem; font-weight:700; color:#fff; white-space:nowrap; }
.method-get { background:#22c55e; }
.method-post { background:#3b82f6; }
.method-put { background:#f59e0b; }
.method-delete { background:#ef4444; }
.doc-path { font-family:monospace; font-size:.9rem; color:var(--text); word-break:break-all; }
.doc-desc { color:var(--text-muted); font-size:.85rem; margin-bottom:.5rem; }
.doc-table { width:100%; border-collapse:collapse; font-size:.82rem; margin-top:.5rem; }
.doc-table th, .doc-table td { text-align:left; padding:4px 8px; border-bottom:1px solid var(--border); }
.doc-table th { color:var(--text-muted); font-weight:600; white-space:nowrap; }
.doc-table td:first-child { font-family:monospace; white-space:nowrap; }
.doc-note { color:var(--text-muted); font-size:.82rem; margin-top:.25rem; }
.doc-json { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:.5rem .75rem; font-family:monospace; font-size:.8rem; margin-top:.5rem; overflow-x:auto; white-space:pre; }
.doc-auth-box { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:1rem; margin:.75rem 0; }
.doc-auth-box code { background:var(--card-bg); padding:2px 6px; border-radius:3px; font-size:.82rem; }
.try-box { margin-top:.5rem; display:flex; align-items:center; gap:.5rem; flex-wrap:wrap; }
.try-box input { flex:1; min-width:200px; font-family:monospace; font-size:.82rem; }
.try-box button { flex-shrink:0; }
.try-result { margin-top:.5rem; background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:.5rem .75rem; font-family:monospace; font-size:.78rem; max-height:300px; overflow:auto; white-space:pre-wrap; word-break:break-all; display:none; }
.try-result.show { display:block; }
.tag-required { color:var(--danger); font-size:.75rem; }
</style>
<div class="section-header">
<h1>{{ 'api_docs.title'|t(user.language) }}</h1>
</div>
<div class="doc-section">
<p style="color:var(--text-muted);font-size:.9rem">{{ 'api_docs.intro'|t(user.language) }}</p>
<p style="margin-top:.5rem"><a href="/settings?tab=apikeys" class="btn btn-sm">{{ 'api_docs.go_settings'|t(user.language) }}</a></p>
</div>
<div class="doc-section">
<h2>{{ 'api_docs.base_url'|t(user.language) }}</h2>
<div class="doc-json">{{ request.base_url }}api/v1</div>
</div>
<div class="doc-section">
<h2>{{ 'api_docs.auth'|t(user.language) }}</h2>
<p class="doc-desc">{{ 'api_docs.auth_desc'|t(user.language) }}</p>
<div class="doc-auth-box">
<code>X-API-Key: mv_your_api_key_here</code>
</div>
</div>
<div class="doc-section">
<h2>{{ 'api_docs.permissions'|t(user.language) }}</h2>
<p class="doc-desc">{{ 'api_docs.permissions_desc'|t(user.language) }}</p>
<table class="doc-table">
<tr><th>{{ 'api_docs.scope'|t(user.language) }}</th><th>{{ 'api_docs.scope_desc'|t(user.language) }}</th><th>{{ 'api_docs.scope_endpoints'|t(user.language) }}</th></tr>
<tr><td><code>profiles:read</code></td><td>{{ 'apikey.perm.profiles_read'|t(user.language) }}</td><td>GET /profiles, GET /profiles/{id}</td></tr>
<tr><td><code>profiles:write</code></td><td>{{ 'apikey.perm.profiles_write'|t(user.language) }}</td><td>POST /profiles, PUT /profiles/{id}, DELETE /profiles/{id}</td></tr>
<tr><td><code>postcards:read</code></td><td>{{ 'apikey.perm.postcards_read'|t(user.language) }}</td><td>GET /profiles/{id}/postcards, GET /profiles/{id}/postcards/{id}</td></tr>
<tr><td><code>postcards:write</code></td><td>{{ 'apikey.perm.postcards_write'|t(user.language) }}</td><td>POST /profiles/{id}/postcards, PUT /profiles/{id}/postcards/{id}, DELETE /profiles/{id}/postcards/{id}</td></tr>
<tr><td><code>images:upload</code></td><td>{{ 'apikey.perm.images_upload'|t(user.language) }}</td><td>POST /profiles/{id}/postcards/{id}/upload</td></tr>
<tr><td colspan="3" style="color:var(--text-muted);font-style:italic">{{ 'api_docs.permissions_keys_note'|t(user.language) }}</td></tr>
</table>
<p class="doc-note" style="margin-top:.5rem">{{ 'api_docs.permissions_note'|t(user.language) }}</p>
</div>
<!-- ═══════ Profiles ═══════ -->
<div class="doc-section">
<h2>{{ 'api_docs.profile_ops'|t(user.language) }}</h2>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-get">GET</span>
<span class="doc-path">/profiles</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.list_profiles'|t(user.language) }}</div>
<div class="doc-json">[
{"id": 1, "nickname": "Japan", "showcase_enabled": false, "showcase_visible": "both", "notes": "", "created_at": "2025-01-01T00:00:00"},
...
]</div>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-post">POST</span>
<span class="doc-path">/profiles</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.create_profile'|t(user.language) }}</div>
<table class="doc-table">
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
<tr><td>nickname</td><td>string <span class="tag-required">{{ 'misc.required'|t(user.language) }}</span></td></tr>
</table>
<div class="doc-json">{"id": 2, "nickname": "Europe", ...}</div>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-get">GET</span>
<span class="doc-path">/profiles/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.get_profile'|t(user.language) }}</div>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-put">PUT</span>
<span class="doc-path">/profiles/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.update_profile'|t(user.language) }}</div>
<table class="doc-table">
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
<tr><td>nickname</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
</table>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-delete">DELETE</span>
<span class="doc-path">/profiles/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.delete_profile'|t(user.language) }}</div>
<div class="doc-json">{"ok": true}</div>
</div>
</div>
<!-- ═══════ Postcards ═══════ -->
<div class="doc-section">
<h2>{{ 'api_docs.postcard_ops'|t(user.language) }}</h2>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-get">GET</span>
<span class="doc-path">/profiles/{profile_id}/postcards</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.list_postcards'|t(user.language) }}</div>
<table class="doc-table">
<tr><th>{{ 'api_docs.params'|t(user.language) }}</th><th></th></tr>
<tr><td>status</td><td>string{{ 'api_docs.ep.optional_str'|t(user.language) }})— {{ 'api_docs.ep.filter_hint'|t(user.language) }}</td></tr>
</table>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-post">POST</span>
<span class="doc-path">/profiles/{profile_id}/postcards</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.create_postcard'|t(user.language) }}</div>
<table class="doc-table">
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
<tr><td>card_number</td><td>string <span class="tag-required">* {{ 'misc.required'|t(user.language) }}</span></td></tr>
<tr><td>status</td><td>string <span class="tag-required">* {{ 'misc.required'|t(user.language) }}</span> — sent / delivered / received</td></tr>
<tr><td>recipient_name</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>country</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }} — {{ 'api_docs.ep.country_hint'|t(user.language) }}</td></tr>
<tr><td>send_time</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }} — {{ 'api_docs.ep.date_hint'|t(user.language) }}</td></tr>
<tr><td>arrival_time</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>sender_name</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>receive_time</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>notes</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
</table>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-get">GET</span>
<span class="doc-path">/profiles/{profile_id}/postcards/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.get_postcard'|t(user.language) }}</div>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-put">PUT</span>
<span class="doc-path">/profiles/{profile_id}/postcards/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.update_postcard'|t(user.language) }}</div>
<table class="doc-table">
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
<tr><td>card_number</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>status</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>recipient_name</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>country</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>send_time</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>arrival_time</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>sender_name</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>receive_time</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
<tr><td>notes</td><td>{{ 'api_docs.ep.optional_str'|t(user.language) }}</td></tr>
</table>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-delete">DELETE</span>
<span class="doc-path">/profiles/{profile_id}/postcards/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.delete_postcard'|t(user.language) }}</div>
<div class="doc-json">{"ok": true}</div>
</div>
</div>
<!-- ═══════ Image Upload ═══════ -->
<div class="doc-section">
<h2>{{ 'api_docs.upload_op'|t(user.language) }}</h2>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-post">POST</span>
<span class="doc-path">/profiles/{profile_id}/postcards/{id}/upload</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.upload_uses'|t(user.language) }} <code>multipart/form-data</code></div>
<table class="doc-table">
<tr><th>Form Field</th><th></th></tr>
<tr><td>file</td><td>File <span class="tag-required">* {{ 'misc.required'|t(user.language) }}</span> — {{ 'api_docs.ep.image_file'|t(user.language) }}</td></tr>
<tr><td>side</td><td>{{ 'api_docs.ep.side_hint'|t(user.language) }}</td></tr>
</table>
</div>
</div>
<!-- ═══════ API Keys ═══════ -->
<div class="doc-section">
<h2>{{ 'api_docs.key_ops'|t(user.language) }}</h2>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-get">GET</span>
<span class="doc-path">/keys</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.list_keys'|t(user.language) }}</div>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-post">POST</span>
<span class="doc-path">/keys</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.create_key'|t(user.language) }}</div>
<table class="doc-table">
<tr><th>{{ 'api_docs.body'|t(user.language) }}</th><th></th></tr>
<tr><td>name</td><td>{{ 'api_docs.ep.key_name_hint'|t(user.language) }}</td></tr>
</table>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-delete">DELETE</span>
<span class="doc-path">/keys/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.ep.delete_key'|t(user.language) }}</div>
<div class="doc-json">{"ok": true}</div>
</div>
</div>
<!-- ═══════ Showcase ═══════ -->
<div class="doc-section">
<h2>{{ 'api_docs.showcase_ops'|t(user.language) }}</h2>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-get">GET</span>
<span class="doc-path">/showcase/{username}</span>
</div>
<div class="doc-desc">{{ 'api_docs.no_auth'|t(user.language) }} — {{ 'api_docs.ep.showcase_user'|t(user.language) }}</div>
</div>
<div class="doc-endpoint">
<div class="ep-header">
<span class="doc-method method-get">GET</span>
<span class="doc-path">/showcase/{username}/profile/{id}</span>
</div>
<div class="doc-desc">{{ 'api_docs.no_auth'|t(user.language) }} — {{ 'api_docs.ep.showcase_profile'|t(user.language) }}</div>
</div>
</div>
<!-- ═══════ Try It ═══════ -->
<div class="doc-section">
<h2>{{ 'api_docs.try_it'|t(user.language) }}</h2>
<div class="try-box">
<input type="text" id="try-key" placeholder="{{ 'api_docs.api_key_ph'|t(user.language) }}">
<select id="try-method" style="width:auto;flex-shrink:0">
<option>GET</option><option>POST</option><option>PUT</option><option>DELETE</option>
</select>
<input type="text" id="try-url" placeholder="/profiles" style="flex:1;min-width:150px;font-family:monospace">
<button class="btn btn-primary btn-sm" onclick="tryApi()">{{ 'api_docs.send'|t(user.language) }}</button>
</div>
<textarea id="try-body" rows="3" placeholder="Request body (JSON, for POST/PUT)" style="width:100%;margin-top:.5rem;font-family:monospace;font-size:.82rem;resize:vertical;display:none"></textarea>
<div class="try-result" id="try-result"></div>
</div>
<script>
document.getElementById('try-method').addEventListener('change', function() {
document.getElementById('try-body').style.display = (this.value === 'POST' || this.value === 'PUT') ? '' : 'none';
});
async function tryApi() {
var key = document.getElementById('try-key').value.trim();
var method = document.getElementById('try-method').value;
var path = document.getElementById('try-url').value.trim();
var body = document.getElementById('try-body').value.trim();
var result = document.getElementById('try-result');
result.className = 'try-result show';
result.textContent = 'Loading...';
try {
var baseUrl = '{{ request.base_url }}api/v1';
var opts = { method: method, headers: {} };
if (key) opts.headers['X-API-Key'] = key;
if (body && (method === 'POST' || method === 'PUT')) {
opts.headers['Content-Type'] = 'application/json';
opts.body = body;
}
var resp = await fetch(baseUrl + path, opts);
var text = await resp.text();
try { text = JSON.stringify(JSON.parse(text), null, 2); } catch(e) {}
result.textContent = resp.status + ' ' + resp.statusText + '\n\n' + text;
} catch(e) {
result.textContent = 'Error: ' + e.message;
}
}
</script>
{% endblock %}