Remove project OAuth hardcodes

This commit is contained in:
Your Name
2026-08-03 17:23:51 +05:00
parent cf12aa0df8
commit f2effe002d
6 changed files with 118 additions and 16 deletions
+5 -1
View File
@@ -2,6 +2,7 @@ APP_ENV=production
APP_SECRET_KEY=change-me
ADMIN_SITE_TITLE=RAA Редакторская
ADMIN_APP_TITLE=RAA Редакторская
ADMIN_FAVICON_PATH=
ADMIN_BOOTSTRAP_LOGIN=admin
ADMIN_BOOTSTRAP_PASSWORD=change-me-long-password
@@ -15,6 +16,9 @@ VK_ACCESS_TOKEN=
VK_GROUP_ACCESS_TOKEN=
VK_API_VERSION=5.199
VK_STORAGE_GROUP_ID=0
VK_OAUTH_CLIENT_ID=
VK_OAUTH_REDIRECT_URI=
VK_OAUTH_ORIGIN=
TG_BOT_TOKEN=
TG_MEDIA_CHANNEL_ID=
@@ -24,4 +28,4 @@ TELEGRAM_API_HASH=
ADMIN_HOST=0.0.0.0
ADMIN_PORT=8080
LOG_LEVEL=INFO
LOG_LEVEL=INFO
+43
View File
@@ -0,0 +1,43 @@
# Multi-project setup
Goal: keep one codebase and run separate deployments for each editorial project.
## Target model
- One canonical git repository for application code.
- One Coolify application per project.
- One Postgres database per project.
- Project differences live in Coolify env variables and database settings, not in code.
## Per-project env
- `ADMIN_SITE_TITLE`
- `ADMIN_APP_TITLE`
- `ADMIN_FAVICON_PATH`
- `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`
- `APP_SECRET_KEY`
- `ADMIN_BOOTSTRAP_LOGIN`, `ADMIN_BOOTSTRAP_PASSWORD`
- `VK_ACCESS_TOKEN`
- `VK_GROUP_ACCESS_TOKEN`
- `VK_STORAGE_GROUP_ID`
- `VK_OAUTH_CLIENT_ID`
- `VK_OAUTH_REDIRECT_URI`
- `VK_OAUTH_ORIGIN`
- `TG_BOT_TOKEN`
- `TG_MEDIA_CHANNEL_ID`
- `LOCAL_BOT_API_URL`
## Per-project database settings
Sources, target groups/channels, poster tokens, schedules, prompts, categories, and enabled workers should stay in `app_settings`, `sources`, `content_categories`, and `worker_controls`.
## Poster modules
The site poster is selected by `site_poster_provider`. A project can disable the worker or leave the provider empty when there is no website poster yet. New website integrations should be added as provider modules behind the same interface instead of branching the worker by project name.
## Migration path
1. Finish removing project-specific constants from code.
2. Choose one canonical repo for both deployments.
3. Point both Coolify applications to that repo and branch.
4. Keep only env and database settings different between projects.
+44 -13
View File
@@ -43,7 +43,7 @@ VK_OAUTH_VERIFIER_COOKIE = "vk_oauth_verifier"
VK_OAUTH_STATE_COOKIE = "vk_oauth_state"
BASE_DIR = Path(__file__).resolve().parent
PROJECT_ROOT = BASE_DIR.parents[1]
FAVICON_PATH = PROJECT_ROOT / "favi.png"
FAVICON_PATH = Path(settings.admin_favicon_path).resolve() if settings.admin_favicon_path.strip() else PROJECT_ROOT / "favi.png"
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
app = FastAPI(title=settings.display_site_title)
LOCAL_TZ = ZoneInfo("Asia/Yekaterinburg")
@@ -54,6 +54,22 @@ EDITOR_MEDIA_DIR.mkdir(parents=True, exist_ok=True)
app.mount("/uploads", StaticFiles(directory=str(UPLOAD_ROOT)), name="uploads")
def public_origin(request: Request) -> str:
configured = settings.vk_oauth_origin.strip().rstrip("/")
if configured:
return configured
proto = request.headers.get("x-forwarded-proto") or request.url.scheme
host = request.headers.get("x-forwarded-host") or request.headers.get("host") or request.url.netloc
return f"{proto}://{host}".rstrip("/")
def oauth_redirect_uri(request: Request, path: str) -> str:
configured = settings.vk_oauth_redirect_uri.strip()
if configured and path == "/vk-oauth/callback":
return configured
return f"{public_origin(request)}{path}"
@app.get("/favi.png")
async def favicon_png():
return FileResponse(FAVICON_PATH)
@@ -882,7 +898,15 @@ def parse_source_line(line: str) -> dict[str, str]:
if not raw:
return {"name": "", "tag": "", "url": "", "error": "Пустая строка"}
parts = re.split(r"\s+", raw)
url_index = next((i for i, part in enumerate(parts) if "vk.com/" in part or part.startswith("club")), -1)
url_index = next(
(
i
for i, part in enumerate(parts)
if any(domain in part for domain in ("vk.com/", "vk.ru/", "m.vk.com/"))
or part.lower().startswith(("club", "public"))
),
-1,
)
if url_index < 0:
if len(parts) == 1:
value = parts[0].strip()
@@ -900,8 +924,9 @@ def parse_source_line(line: str) -> dict[str, str]:
else:
name = ""
tag = ""
if url.startswith("vk.com/"):
if url.startswith(("vk.com/", "vk.ru/", "m.vk.com/")):
url = f"https://{url}"
url = url.replace("https://vk.ru/", "https://vk.com/", 1).replace("https://m.vk.com/", "https://vk.com/", 1)
return {"name": name, "tag": normalize_hash_tag(tag, ""), "url": url, "error": ""}
@@ -1757,19 +1782,22 @@ async def vk_oauth_callback(request: Request, code: str = "", error: str = "", e
@app.get("/vk/oauth/start")
async def vk_oauth_start() -> RedirectResponse:
async def vk_oauth_start(request: Request) -> RedirectResponse:
client_id = settings.vk_oauth_client_id.strip() or str(await fetch_setting("vk_poster_app_id", "") or "").strip()
if not client_id:
return redirect("/workers")
verifier = secrets.token_urlsafe(64)
state = secrets.token_urlsafe(24)
redirect_uri = "https://sw.exostring.xyz/vk/oauth/callback"
redirect_uri = oauth_redirect_uri(request, "/vk/oauth/callback")
params = {
"client_id": "54635120",
"client_id": client_id,
"redirect_uri": redirect_uri,
"response_type": "code",
"scope": "wall photos video groups offline",
"state": state,
"code_challenge": pkce_challenge(verifier),
"code_challenge_method": "s256",
"origin": "https://sw.exostring.xyz",
"origin": public_origin(request),
"v": "5.199",
}
response = RedirectResponse(f"https://id.vk.ru/authorize?{urlencode(params)}")
@@ -1779,13 +1807,16 @@ async def vk_oauth_start() -> RedirectResponse:
@app.get("/vk/group-oauth/start")
async def vk_group_oauth_start() -> RedirectResponse:
async def vk_group_oauth_start(request: Request) -> RedirectResponse:
client_id = settings.vk_oauth_client_id.strip() or str(await fetch_setting("vk_poster_app_id", "") or "").strip()
if not client_id:
return redirect("/workers")
group_id = abs(int(settings.vk_storage_group_id))
params = {
"client_id": "54635120",
"client_id": client_id,
"group_ids": str(group_id),
"display": "page",
"redirect_uri": "https://sw.exostring.xyz/vk/oauth/callback",
"redirect_uri": oauth_redirect_uri(request, "/vk/oauth/callback"),
"scope": "manage,photos,docs",
"response_type": "token",
"state": secrets.token_urlsafe(24),
@@ -1804,7 +1835,7 @@ async def vk_poster_oauth_start(request: Request) -> RedirectResponse:
return redirect("/workers")
verifier = secrets.token_urlsafe(64)
state = secrets.token_urlsafe(24)
redirect_uri = "https://sw.exostring.xyz/vk-oauth/callback"
redirect_uri = oauth_redirect_uri(request, "/vk-oauth/callback")
params = {
"client_id": client_id,
"redirect_uri": redirect_uri,
@@ -1813,7 +1844,7 @@ async def vk_poster_oauth_start(request: Request) -> RedirectResponse:
"state": state,
"code_challenge": pkce_challenge(verifier),
"code_challenge_method": "s256",
"origin": "https://sw.exostring.xyz",
"origin": public_origin(request),
"v": "5.199",
}
response = RedirectResponse(f"https://id.vk.ru/authorize?{urlencode(params)}")
@@ -1850,7 +1881,7 @@ async def vk_poster_oauth_callback(
client_id = str(await fetch_setting("vk_poster_app_id", "") or "").strip()
client_secret = str(await fetch_setting("vk_poster_client_secret", "") or "").strip()
owner_id = int(await fetch_int_setting("vk_poster_owner_id", 0))
redirect_uri = "https://sw.exostring.xyz/vk-oauth/callback"
redirect_uri = oauth_redirect_uri(request, "/vk-oauth/callback")
code_verifier = request.cookies.get(VK_OAUTH_VERIFIER_COOKIE, "")
device_id = request.query_params.get("device_id", "")
try:
+4
View File
@@ -8,6 +8,7 @@ class Settings(BaseSettings):
app_secret_key: str = "change-me"
admin_site_title: str = ""
admin_app_title: str = "Редакторская"
admin_favicon_path: str = ""
admin_bootstrap_login: str = "admin"
admin_bootstrap_password: str = ""
@@ -21,6 +22,9 @@ class Settings(BaseSettings):
vk_group_access_token: str = ""
vk_api_version: str = "5.199"
vk_storage_group_id: int = 0
vk_oauth_client_id: str = ""
vk_oauth_redirect_uri: str = ""
vk_oauth_origin: str = ""
tg_bot_token: str = ""
tg_media_channel_id: str = ""
+18
View File
@@ -393,4 +393,22 @@
</details>
{% endfor %}
</div>
<script>
(() => {
const scrollKey = "workers-scroll-y";
const restoreY = sessionStorage.getItem(scrollKey);
if (restoreY !== null) {
sessionStorage.removeItem(scrollKey);
requestAnimationFrame(() => window.scrollTo(0, Number(restoreY) || 0));
}
document.addEventListener("submit", (event) => {
const form = event.target;
if (!(form instanceof HTMLFormElement)) return;
const action = form.getAttribute("action") || "";
if (action.startsWith("/workers") || action.startsWith("/settings") || action.includes("-schedule/")) {
sessionStorage.setItem(scrollKey, String(window.scrollY));
}
}, true);
})();
</script>
{% endblock %}
+4 -2
View File
@@ -192,8 +192,10 @@ def normalize_vk_source(value: str) -> str:
raw = str(value or "").strip()
if not raw:
return ""
if "vk.com" in raw:
raw = raw.split("vk.com", 1)[1]
for host in ("vk.com", "vk.ru", "m.vk.com"):
if host in raw:
raw = raw.split(host, 1)[1]
break
raw = raw.lstrip("/")
raw = raw.split("?", 1)[0].split("#", 1)[0].strip()
raw = re.sub(r"^(club|public)", "", raw, flags=re.IGNORECASE)