Compare commits
4 Commits
407a0c15e1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dab2685498 | ||
|
|
2d9cb4581a | ||
|
|
c54ba02253 | ||
|
|
6afc464d53 |
@@ -52,20 +52,27 @@ def _check_auth(request: Request) -> bool:
|
|||||||
|
|
||||||
def _require_auth(request: Request) -> None:
|
def _require_auth(request: Request) -> None:
|
||||||
if not _check_auth(request):
|
if not _check_auth(request):
|
||||||
|
from fastapi.responses import RedirectResponse
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
|
||||||
|
def _redirect_if_unauth(request: Request):
|
||||||
|
if not _check_auth(request):
|
||||||
|
return RedirectResponse("/login", status_code=303)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@app.get("/login", response_class=HTMLResponse)
|
@app.get("/login", response_class=HTMLResponse)
|
||||||
async def login_page(request: Request):
|
async def login_page(request: Request):
|
||||||
return TEMPLATES.TemplateResponse("login.html", {"request": request})
|
return TEMPLATES.TemplateResponse(request, "login.html")
|
||||||
|
|
||||||
|
|
||||||
@app.post("/login")
|
@app.post("/login")
|
||||||
async def login(request: Request, token: str = Form(...)):
|
async def login(request: Request, token: str = Form(...)):
|
||||||
if token != AUTH_TOKEN:
|
if token != AUTH_TOKEN:
|
||||||
return TEMPLATES.TemplateResponse("login.html", {"request": request, "error": "Invalid token"})
|
return TEMPLATES.TemplateResponse(request, "login.html", {"error": "Invalid token"})
|
||||||
resp = RedirectResponse("/", status_code=303)
|
resp = RedirectResponse("/", status_code=303)
|
||||||
resp.set_cookie("token", token, httponly=True, samesite="lax")
|
resp.set_cookie("token", token, httponly=True, samesite="lax")
|
||||||
return resp
|
return resp
|
||||||
@@ -82,13 +89,13 @@ async def logout():
|
|||||||
|
|
||||||
@app.get("/", response_class=HTMLResponse)
|
@app.get("/", response_class=HTMLResponse)
|
||||||
async def index(request: Request):
|
async def index(request: Request):
|
||||||
_require_auth(request)
|
if redir := _redirect_if_unauth(request):
|
||||||
|
return redir
|
||||||
all_profiles = db.list_profiles()
|
all_profiles = db.list_profiles()
|
||||||
for p in all_profiles:
|
for p in all_profiles:
|
||||||
p["running"] = pm.is_running(p["id"])
|
p["running"] = pm.is_running(p["id"])
|
||||||
p["config"] = json.loads(p.get("config") or "{}")
|
p["config"] = json.loads(p.get("config") or "{}")
|
||||||
return TEMPLATES.TemplateResponse("index.html", {
|
return TEMPLATES.TemplateResponse(request, "index.html", {
|
||||||
"request": request,
|
|
||||||
"profiles": all_profiles,
|
"profiles": all_profiles,
|
||||||
"bot_types": pm.BOT_TYPES,
|
"bot_types": pm.BOT_TYPES,
|
||||||
})
|
})
|
||||||
@@ -96,7 +103,8 @@ async def index(request: Request):
|
|||||||
|
|
||||||
@app.get("/profile/{profile_id}", response_class=HTMLResponse)
|
@app.get("/profile/{profile_id}", response_class=HTMLResponse)
|
||||||
async def profile_page(request: Request, profile_id: int):
|
async def profile_page(request: Request, profile_id: int):
|
||||||
_require_auth(request)
|
if redir := _redirect_if_unauth(request):
|
||||||
|
return redir
|
||||||
profile = db.get_profile(profile_id)
|
profile = db.get_profile(profile_id)
|
||||||
if not profile:
|
if not profile:
|
||||||
raise HTTPException(404, "Profile not found")
|
raise HTTPException(404, "Profile not found")
|
||||||
@@ -106,8 +114,7 @@ async def profile_page(request: Request, profile_id: int):
|
|||||||
contacts = bot.contacts if bot else []
|
contacts = bot.contacts if bot else []
|
||||||
groups = bot.groups if bot else []
|
groups = bot.groups if bot else []
|
||||||
log_lines = bot.log_lines[-50:] if bot else []
|
log_lines = bot.log_lines[-50:] if bot else []
|
||||||
return TEMPLATES.TemplateResponse("profile.html", {
|
return TEMPLATES.TemplateResponse(request, "profile.html", {
|
||||||
"request": request,
|
|
||||||
"profile": profile,
|
"profile": profile,
|
||||||
"contacts": contacts,
|
"contacts": contacts,
|
||||||
"groups": groups,
|
"groups": groups,
|
||||||
|
|||||||
@@ -3,17 +3,28 @@ set -e
|
|||||||
|
|
||||||
cd "$(dirname "$0")"
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
if [ ! -d ".venv" ]; then
|
# Bootstrap virtualenv — check for uvicorn specifically so partial installs are retried
|
||||||
echo "Creating virtualenv..."
|
if [ ! -f ".venv/bin/uvicorn" ]; then
|
||||||
python3.12 -m venv .venv
|
echo "Setting up virtualenv..."
|
||||||
.venv/bin/pip install -q -r requirements.txt
|
rm -rf .venv
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/pip install --upgrade pip
|
||||||
|
.venv/bin/pip install -r requirements.txt
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p data/bots
|
mkdir -p data/bots static
|
||||||
|
|
||||||
|
# Pre-download the libsimplex native binary so first Start doesn't stall
|
||||||
|
echo "Checking simplex-chat binary..."
|
||||||
|
.venv/bin/python -m simplex_chat install 2>/dev/null && echo " simplex binary ready." || echo " Binary not downloaded yet — use the Init button in Settings."
|
||||||
|
|
||||||
|
# Set token — override via: MANAGER_TOKEN=mysecret ./start.sh
|
||||||
export MANAGER_TOKEN="${MANAGER_TOKEN:-changeme}"
|
export MANAGER_TOKEN="${MANAGER_TOKEN:-changeme}"
|
||||||
|
|
||||||
echo "Starting SimpleX Manager on http://0.0.0.0:8000"
|
echo ""
|
||||||
|
echo " SimpleX Manager"
|
||||||
|
echo " URL: http://0.0.0.0:8000"
|
||||||
echo " Token: $MANAGER_TOKEN"
|
echo " Token: $MANAGER_TOKEN"
|
||||||
|
echo ""
|
||||||
|
|
||||||
exec .venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
exec .venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000
|
||||||
|
|||||||
@@ -5,6 +5,13 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}SimpleX Manager{% endblock %}</title>
|
<title>{% block title %}SimpleX Manager{% endblock %}</title>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>
|
<script src="https://unpkg.com/htmx.org@1.9.12/dist/htmx.min.js"></script>
|
||||||
|
<script>
|
||||||
|
// Inject auth token into every HTMX request automatically
|
||||||
|
document.addEventListener('htmx:configRequest', function(evt) {
|
||||||
|
const m = document.cookie.match(/(?:^|;\s*)token=([^;]+)/);
|
||||||
|
if (m) evt.detail.headers['X-Token'] = m[1];
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<style>
|
<style>
|
||||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
|||||||
@@ -28,12 +28,10 @@
|
|||||||
<a href="/profile/{{ p.id }}" class="btn btn-ghost" style="padding: 6px 14px; font-size: 13px;">View</a>
|
<a href="/profile/{{ p.id }}" class="btn btn-ghost" style="padding: 6px 14px; font-size: 13px;">View</a>
|
||||||
<button class="btn btn-success" style="padding: 6px 14px; font-size: 13px;"
|
<button class="btn btn-success" style="padding: 6px 14px; font-size: 13px;"
|
||||||
hx-post="/api/profiles/{{ p.id }}/start"
|
hx-post="/api/profiles/{{ p.id }}/start"
|
||||||
hx-headers='{"X-Token": "{{ request.cookies.get(\"token\", \"\") }}"}'
|
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
onclick="this.textContent='Starting…'">Start</button>
|
onclick="this.textContent='Starting…'">Start</button>
|
||||||
<button class="btn btn-danger" style="padding: 6px 14px; font-size: 13px;"
|
<button class="btn btn-danger" style="padding: 6px 14px; font-size: 13px;"
|
||||||
hx-post="/api/profiles/{{ p.id }}/stop"
|
hx-post="/api/profiles/{{ p.id }}/stop"
|
||||||
hx-headers='{"X-Token": "{{ request.cookies.get(\"token\", \"\") }}"}'
|
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
onclick="this.textContent='Stopping…'">Stop</button>
|
onclick="this.textContent='Stopping…'">Stop</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,9 +93,10 @@ document.getElementById('create-form').addEventListener('submit', async (e) => {
|
|||||||
const welcome = fd.get('welcome_message')
|
const welcome = fd.get('welcome_message')
|
||||||
if (welcome) config.welcome_message = welcome
|
if (welcome) config.welcome_message = welcome
|
||||||
|
|
||||||
|
const token = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || ''
|
||||||
const resp = await fetch('/api/profiles', {
|
const resp = await fetch('/api/profiles', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json', 'X-Token': document.cookie.match(/token=([^;]+)/)?.[1] || ''},
|
headers: {'Content-Type': 'application/json', 'X-Token': token},
|
||||||
body: JSON.stringify({name: fd.get('name'), bot_type: fd.get('bot_type'), config})
|
body: JSON.stringify({name: fd.get('name'), bot_type: fd.get('bot_type'), config})
|
||||||
})
|
})
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
|
|||||||
@@ -24,13 +24,11 @@
|
|||||||
{% if profile.running %}
|
{% if profile.running %}
|
||||||
<button class="btn btn-danger"
|
<button class="btn btn-danger"
|
||||||
hx-post="/api/profiles/{{ profile.id }}/stop"
|
hx-post="/api/profiles/{{ profile.id }}/stop"
|
||||||
hx-headers='{"X-Token": "{{ request.cookies.get(\"token\", \"\") }}"}'
|
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on::after-request="location.reload()">Stop</button>
|
hx-on::after-request="location.reload()">Stop</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button class="btn btn-success"
|
<button class="btn btn-success"
|
||||||
hx-post="/api/profiles/{{ profile.id }}/start"
|
hx-post="/api/profiles/{{ profile.id }}/start"
|
||||||
hx-headers='{"X-Token": "{{ request.cookies.get(\"token\", \"\") }}"}'
|
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on::after-request="location.reload()">Start</button>
|
hx-on::after-request="location.reload()">Start</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -137,7 +135,6 @@
|
|||||||
<h2 style="margin:0;">Event Log</h2>
|
<h2 style="margin:0;">Event Log</h2>
|
||||||
<button class="btn btn-ghost" style="font-size:12px;padding:4px 10px;"
|
<button class="btn btn-ghost" style="font-size:12px;padding:4px 10px;"
|
||||||
hx-get="/api/profiles/{{ profile.id }}/status"
|
hx-get="/api/profiles/{{ profile.id }}/status"
|
||||||
hx-headers='{"X-Token": "{{ request.cookies.get(\"token\", \"\") }}"}'
|
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-on::after-request="refreshLog(event)">Refresh</button>
|
hx-on::after-request="refreshLog(event)">Refresh</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -153,9 +150,10 @@ document.getElementById('send-form').addEventListener('submit', async (e) => {
|
|||||||
const fd = new FormData(e.target)
|
const fd = new FormData(e.target)
|
||||||
const result = document.getElementById('send-result')
|
const result = document.getElementById('send-result')
|
||||||
result.textContent = 'Sending…'
|
result.textContent = 'Sending…'
|
||||||
|
const token = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || ''
|
||||||
const resp = await fetch('/api/profiles/{{ profile.id }}/send', {
|
const resp = await fetch('/api/profiles/{{ profile.id }}/send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {'Content-Type': 'application/json', 'X-Token': document.cookie.match(/token=([^;]+)/)?.[1] || ''},
|
headers: {'Content-Type': 'application/json', 'X-Token': token},
|
||||||
body: JSON.stringify({to: fd.get('to'), text: fd.get('text')})
|
body: JSON.stringify({to: fd.get('to'), text: fd.get('text')})
|
||||||
})
|
})
|
||||||
const data = await resp.json()
|
const data = await resp.json()
|
||||||
@@ -174,9 +172,10 @@ function refreshLog(event) {
|
|||||||
|
|
||||||
function confirmDelete() {
|
function confirmDelete() {
|
||||||
if (!confirm('Delete this profile? This cannot be undone.')) return
|
if (!confirm('Delete this profile? This cannot be undone.')) return
|
||||||
|
const token = document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || ''
|
||||||
fetch('/api/profiles/{{ profile.id }}', {
|
fetch('/api/profiles/{{ profile.id }}', {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {'X-Token': document.cookie.match(/token=([^;]+)/)?.[1] || ''}
|
headers: {'X-Token': token}
|
||||||
}).then(() => location.href = '/')
|
}).then(() => location.href = '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user