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,7 @@
|
||||
"use client";
|
||||
|
||||
import { IntegrationsAdmin } from "@/components/admin/IntegrationsAdmin";
|
||||
|
||||
export default function Page() {
|
||||
return <IntegrationsAdmin />;
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export default async function AdminLayout({
|
||||
{ href: "/admin/files", label: t("media") },
|
||||
{ href: "/admin/ai", label: t("aiContent") },
|
||||
{ href: "/admin/messaging", label: t("messaging") },
|
||||
{ href: "/admin/integrations", label: t("integrations") },
|
||||
{ href: "/admin/marketing", label: t("marketing") },
|
||||
{ href: "/admin/crm", label: t("crm") },
|
||||
{ href: "/admin/users", label: t("users") },
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
/**
|
||||
* OAuth landing page. Identity redirects here with tokens in the URL fragment
|
||||
* (#access_token=…&refresh_token=…&expires_in=…). We hand them to a server route
|
||||
* that sets httpOnly session cookies, then continue to the app.
|
||||
*/
|
||||
export default function OAuthCallbackPage() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const hash = typeof window !== "undefined" ? window.location.hash.replace(/^#/, "") : "";
|
||||
const params = new URLSearchParams(hash);
|
||||
const accessToken = params.get("access_token");
|
||||
const refreshToken = params.get("refresh_token");
|
||||
const expiresIn = Number(params.get("expires_in") ?? "900");
|
||||
|
||||
if (!accessToken || !refreshToken) {
|
||||
setError("ورود ناموفق بود. لطفاً دوباره تلاش کنید.");
|
||||
return;
|
||||
}
|
||||
|
||||
const next = searchParams.get("next") || "/dashboard";
|
||||
(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/auth/oauth-session", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn }),
|
||||
});
|
||||
if (!res.ok) throw new Error("session");
|
||||
// Clear the fragment from history, then continue.
|
||||
window.history.replaceState(null, "", window.location.pathname);
|
||||
router.replace(next.startsWith("/") ? next : "/dashboard");
|
||||
router.refresh();
|
||||
} catch {
|
||||
setError("برقراری نشست ناموفق بود.");
|
||||
}
|
||||
})();
|
||||
}, [router, searchParams]);
|
||||
|
||||
return (
|
||||
<main className="flex min-h-screen items-center justify-center bg-neutral-50 text-neutral-700" dir="rtl">
|
||||
<div className="text-center">
|
||||
{error ? (
|
||||
<>
|
||||
<p className="text-sm text-red-600">{error}</p>
|
||||
<a href="/auth" className="mt-3 inline-block text-sm text-primary-600 hover:underline">بازگشت به ورود</a>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm">در حال ورود…</p>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user