Add VK callback auth support and admin demotion
Some checks are pending
Desktop Release / release (push) Waiting to run
Desktop CI / tests (push) Successful in 1m51s

This commit is contained in:
Денисов Александр Андреевич
2026-06-05 19:01:52 +03:00
parent 5a3e4c188e
commit 0a82ad7e3e
10 changed files with 520 additions and 42 deletions

View File

@@ -0,0 +1,35 @@
server {
listen 80;
server_name vk.daemonlord.ru;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name vk.daemonlord.ru;
ssl_certificate /etc/letsencrypt/live/vk.daemonlord.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/vk.daemonlord.ru/privkey.pem;
location = /vk/callback {
proxy_pass http://127.0.0.1:8787/vk/callback;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_buffering off;
}
location = /health {
proxy_pass http://127.0.0.1:8787/health;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
location / {
return 404;
}
}

View File

@@ -0,0 +1,15 @@
[Unit]
Description=Anabasis VK OAuth callback host
After=network.target
[Service]
Type=simple
WorkingDirectory=/opt/anabasis-chat-remove
ExecStart=/usr/bin/python3 /opt/anabasis-chat-remove/deploy/vk_callback_server.py --host 127.0.0.1 --port 8787 --quiet
Restart=always
RestartSec=3
User=www-data
Group=www-data
[Install]
WantedBy=multi-user.target

81
deploy/vk-auth-setup.md Normal file
View File

@@ -0,0 +1,81 @@
# VK OAuth callback setup
The desktop app uses this redirect URI by default:
```text
https://vk.daemonlord.ru/vk/callback
```
The public HTTPS endpoint is expected to be handled by a reverse proxy. The
backend callback host itself is plain HTTP on a non-standard local port:
```text
http://127.0.0.1:8787/vk/callback
```
## VK app settings
1. Open the VK developer dashboard.
2. Select the standalone app with ID `54454043`.
3. Make sure the app type is `Standalone application`.
4. Add the exact redirect URI:
```text
https://vk.daemonlord.ru/vk/callback
```
If the redirect URI in the OAuth request does not exactly match the app settings,
VK can return:
```json
{"error":"invalid_request","error_description":"Security Error"}
```
## Backend callback host
Run the local HTTP callback host:
```bash
python deploy/vk_callback_server.py --host 127.0.0.1 --port 8787
```
Health check:
```text
http://127.0.0.1:8787/health
```
Optional systemd unit:
```text
deploy/systemd/anabasis-vk-callback.service
```
Adjust `WorkingDirectory`, `ExecStart`, and `User` for the server path/user.
The callback page does not process or store the token. With implicit OAuth, VK
puts `access_token` in the URL fragment. The desktop webview reads that final URL
directly from the embedded browser.
## Reverse proxy
Use the nginx example:
```text
deploy/nginx/vk.daemonlord.ru.conf
```
It proxies:
```text
https://vk.daemonlord.ru/vk/callback -> http://127.0.0.1:8787/vk/callback
```
## Desktop app override
The app already defaults to `https://vk.daemonlord.ru/vk/callback`. To override it:
```powershell
$env:ANABASIS_VK_REDIRECT_URI = "https://vk.daemonlord.ru/vk/callback"
python main.py
```

View File

@@ -0,0 +1,46 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VK authorization</title>
<style>
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
font-family: Arial, sans-serif;
color: #1f2937;
background: #f8fafc;
}
main {
width: min(520px, calc(100vw - 32px));
padding: 24px;
border: 1px solid #dbe3ef;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08);
}
h1 {
margin: 0 0 12px;
font-size: 20px;
line-height: 1.3;
}
p {
margin: 0;
font-size: 15px;
line-height: 1.5;
}
</style>
</head>
<body>
<main>
<h1>Авторизация VK завершена</h1>
<p>Вернитесь в приложение. Это окно можно закрыть после завершения входа.</p>
</main>
</body>
</html>

View File

@@ -0,0 +1,78 @@
import argparse
import os
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 8787
CALLBACK_PATH = "/vk/callback"
HEALTH_PATH = "/health"
def _read_callback_html():
html_path = Path(__file__).resolve().parent / "vk-callback" / "index.html"
return html_path.read_bytes()
class VkCallbackHandler(BaseHTTPRequestHandler):
server_version = "AnabasisVkCallback/1.0"
def do_GET(self):
path = self.path.split("?", 1)[0]
if path == HEALTH_PATH:
self._send_text(200, "ok\n")
return
if path in ("/", CALLBACK_PATH):
self._send_html(200, _read_callback_html())
return
self._send_text(404, "not found\n")
def log_message(self, fmt, *args):
if getattr(self.server, "quiet", False):
return
super().log_message(fmt, *args)
def _send_html(self, status, body):
self.send_response(status)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def _send_text(self, status, text):
body = text.encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def parse_args():
parser = argparse.ArgumentParser(description="Anabasis VK OAuth callback host")
parser.add_argument("--host", default=os.getenv("ANABASIS_VK_CALLBACK_HOST", DEFAULT_HOST))
parser.add_argument("--port", type=int, default=int(os.getenv("ANABASIS_VK_CALLBACK_PORT", DEFAULT_PORT)))
parser.add_argument("--quiet", action="store_true")
return parser.parse_args()
def main():
args = parse_args()
server = ThreadingHTTPServer((args.host, args.port), VkCallbackHandler)
server.quiet = args.quiet
print(f"VK callback server listening on http://{args.host}:{args.port}{CALLBACK_PATH}")
print(f"Health check: http://{args.host}:{args.port}{HEALTH_PATH}")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
return 0
if __name__ == "__main__":
raise SystemExit(main())