feat(auth+admin): Sign in with Google (OAuth) + Integrations config panel
Build backend images / build content-svc (push) Failing after 1m2s
Build backend images / build file-svc (push) Failing after 3m11s
Build backend images / build gateway (push) Failing after 5m39s
Build backend images / build identity-svc (push) Failing after 38s
Build backend images / build notification-svc (push) Failing after 2m0s
Build backend images / build render-svc (push) Failing after 58s
Build backend images / build studio-svc (push) Failing after 58s
Build backend images / build content-svc (push) Failing after 1m2s
Build backend images / build file-svc (push) Failing after 3m11s
Build backend images / build gateway (push) Failing after 5m39s
Build backend images / build identity-svc (push) Failing after 38s
Build backend images / build notification-svc (push) Failing after 2m0s
Build backend images / build render-svc (push) Failing after 58s
Build backend images / build studio-svc (push) Failing after 58s
Backend (identity-svc):
- oauth_config table (mig 22) + OAuthConfig entity
- OAuthService: admin config CRUD + Google authorization-code flow (build consent
URL, exchange code, fetch userinfo, find/create RegisterMode.Google user, issue
session via AuthService.IssueOAuthSessionAsync)
- AuthController: GET /v1/auth/google/{start,callback} (public); tokens handed to
frontend via URL fragment
- AdminController: GET/PUT /v1/admin/oauth/{provider} (admin, secret masked)
Frontend:
- "ورود با گوگل" button on /auth → identity start endpoint
- /auth/callback reads fragment tokens → /api/auth/oauth-session sets httpOnly cookies
- /admin/integrations: Google client_id/secret/redirect_uri + enable, with setup guide
- nav + fa/en labels
Client ID/Secret are configured entirely in the admin panel — no redeploy needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
const card = "rounded-xl border border-[#1e2235] bg-[#0f1120] p-5";
|
||||
const btn = "rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50";
|
||||
const inp = "w-full rounded-lg border border-[#262b40] bg-[#0c0e1a] px-3 py-2 text-sm text-gray-100 outline-none focus:border-indigo-500";
|
||||
const lbl = "mb-1 block text-xs font-medium text-gray-400";
|
||||
|
||||
export function IntegrationsAdmin() {
|
||||
const [clientId, setClientId] = useState("");
|
||||
const [clientSecret, setClientSecret] = useState("");
|
||||
const [redirectUri, setRedirectUri] = useState("");
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [hasSecret, setHasSecret] = useState(false);
|
||||
const [msg, setMsg] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Suggested redirect URI = gateway base (…/v1) + /auth/google/callback
|
||||
const suggested = ((process.env.NEXT_PUBLIC_API_URL ?? "") + "/auth/google/callback").replace(/([^:])\/\//g, "$1/");
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const r = await fetch("/api/admin/resource/admin/oauth/google", { cache: "no-store" }).then((x) => x.json()).catch(() => null);
|
||||
if (r) {
|
||||
setClientId(r.client_id ?? "");
|
||||
setRedirectUri(r.redirect_uri ?? "");
|
||||
setEnabled(!!r.enabled);
|
||||
setHasSecret(!!r.has_secret);
|
||||
}
|
||||
}, []);
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const save = async () => {
|
||||
setSaving(true); setMsg(null);
|
||||
const res = await fetch("/api/admin/resource/admin/oauth/google", {
|
||||
method: "PUT", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
client_id: clientId,
|
||||
client_secret: clientSecret || null, // blank keeps existing
|
||||
redirect_uri: redirectUri || suggested,
|
||||
enabled,
|
||||
}),
|
||||
});
|
||||
const d = await res.json().catch(() => null);
|
||||
setMsg(res.ok ? "ذخیره شد ✓" : (d?.error?.message ?? "خطا در ذخیره"));
|
||||
setSaving(false);
|
||||
if (res.ok) { setClientSecret(""); load(); }
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6" dir="rtl">
|
||||
<div>
|
||||
<h1 className="text-xl font-semibold text-white">یکپارچهسازیها</h1>
|
||||
<p className="mt-1 text-sm text-gray-400">پیکربندی ورود با حسابهای خارجی. کلیدها را از کنسول مربوطه دریافت و اینجا وارد کنید.</p>
|
||||
</div>
|
||||
|
||||
<section className={card}>
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-sm font-semibold text-white">ورود با گوگل (Google OAuth)</h2>
|
||||
<label className="flex items-center gap-2 text-sm text-gray-300">
|
||||
<input type="checkbox" checked={enabled} onChange={(e) => setEnabled(e.target.checked)} /> فعال
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-3">
|
||||
<div>
|
||||
<label className={lbl}>Client ID</label>
|
||||
<input className={inp} value={clientId} onChange={(e) => setClientId(e.target.value)} placeholder="xxxxxx.apps.googleusercontent.com" dir="ltr" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Client Secret {hasSecret && <span className="text-emerald-400">(ذخیرهشده — برای حفظ خالی بگذارید)</span>}</label>
|
||||
<input className={inp} type="password" value={clientSecret} onChange={(e) => setClientSecret(e.target.value)} placeholder={hasSecret ? "••••••••" : "GOCSPX-…"} dir="ltr" />
|
||||
</div>
|
||||
<div>
|
||||
<label className={lbl}>Redirect URI (در کنسول گوگل ثبت کنید)</label>
|
||||
<input className={inp} value={redirectUri} onChange={(e) => setRedirectUri(e.target.value)} placeholder={suggested} dir="ltr" />
|
||||
<p className="mt-1 text-[11px] text-gray-500">مقدار پیشنهادی: <code className="text-gray-400" dir="ltr">{suggested}</code></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center gap-3">
|
||||
<button className={btn} onClick={save} disabled={saving || !clientId}>{saving ? "در حال ذخیره…" : "ذخیره تنظیمات"}</button>
|
||||
{msg && <span className="text-xs text-gray-400">{msg}</span>}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-lg border border-[#262b40] bg-[#0c0e1a] p-3 text-xs leading-6 text-gray-400">
|
||||
<p className="font-medium text-gray-300">راهنمای سریع:</p>
|
||||
<p>۱) به <span dir="ltr">console.cloud.google.com</span> بروید و یک «OAuth 2.0 Client ID» از نوع Web بسازید.</p>
|
||||
<p>۲) مقدار Redirect URI بالا را در «Authorized redirect URIs» ثبت کنید.</p>
|
||||
<p>۳) Client ID و Secret را اینجا وارد کنید، «فعال» را بزنید و ذخیره کنید.</p>
|
||||
<p>۴) دکمهٔ «ورود با گوگل» در صفحهٔ ورود ظاهر و فعال میشود.</p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user