Remove Apple SSO, keep Google + GitHub only
- Removed Apple from OAuth lib (config, exchange, provider type) - Removed Apple button from auth page - Removed Apple tab from setup guide page - Removed Apple env vars from .env, .env.example, docker-compose.yml - Removed Apple prompts from setup-sso.sh
This commit is contained in:
@@ -11,8 +11,6 @@ services:
|
|||||||
- DATABASE_URL=file:./dev.db
|
- DATABASE_URL=file:./dev.db
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
|
||||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
|
||||||
- APPLE_CLIENT_ID=${APPLE_CLIENT_ID:-}
|
|
||||||
- APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET:-}
|
|
||||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
|
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
|
||||||
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
|
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+1
-13
@@ -33,19 +33,11 @@ read -sp " Client Secret: " GOOGLE_CLIENT_SECRET
|
|||||||
echo ""
|
echo ""
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# --- Prompt for Apple ---
|
|
||||||
echo "── 🍎 Apple Sign In ──"
|
|
||||||
read -p " Service ID : " APPLE_CLIENT_ID
|
|
||||||
read -sp " Client Secret: " APPLE_CLIENT_SECRET
|
|
||||||
echo ""
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# --- Confirm ---
|
# --- Confirm ---
|
||||||
echo "═══════════════════════════════════════════════"
|
echo "═══════════════════════════════════════════════"
|
||||||
echo " Summary:"
|
echo " Summary:"
|
||||||
echo " GitHub: ${GITHUB_CLIENT_ID:0:10}..."
|
echo " GitHub: ${GITHUB_CLIENT_ID:0:10}..."
|
||||||
echo " Google: ${GOOGLE_CLIENT_ID:0:10}..."
|
echo " Google: ${GOOGLE_CLIENT_ID:0:10}..."
|
||||||
echo " Apple: $APPLE_CLIENT_ID"
|
|
||||||
echo "═══════════════════════════════════════════════"
|
echo "═══════════════════════════════════════════════"
|
||||||
read -p "Apply these credentials and redeploy? (y/N) " CONFIRM
|
read -p "Apply these credentials and redeploy? (y/N) " CONFIRM
|
||||||
|
|
||||||
@@ -56,13 +48,11 @@ fi
|
|||||||
|
|
||||||
# --- Backup current .env ---
|
# --- Backup current .env ---
|
||||||
cp "$ENV_FILE" "$ENV_FILE.backup"
|
cp "$ENV_FILE" "$ENV_FILE.backup"
|
||||||
echo "✓ Backed up .env → .env.backup"
|
echo "✓ Backed up .env -> .env.backup"
|
||||||
|
|
||||||
# --- Remove old OAuth lines if they exist ---
|
# --- Remove old OAuth lines if they exist ---
|
||||||
sed -i '/^GOOGLE_CLIENT_ID=/d' "$ENV_FILE"
|
sed -i '/^GOOGLE_CLIENT_ID=/d' "$ENV_FILE"
|
||||||
sed -i '/^GOOGLE_CLIENT_SECRET=/d' "$ENV_FILE"
|
sed -i '/^GOOGLE_CLIENT_SECRET=/d' "$ENV_FILE"
|
||||||
sed -i '/^APPLE_CLIENT_ID=/d' "$ENV_FILE"
|
|
||||||
sed -i '/^APPLE_CLIENT_SECRET=/d' "$ENV_FILE"
|
|
||||||
sed -i '/^GITHUB_CLIENT_ID=/d' "$ENV_FILE"
|
sed -i '/^GITHUB_CLIENT_ID=/d' "$ENV_FILE"
|
||||||
sed -i '/^GITHUB_CLIENT_SECRET=/d' "$ENV_FILE"
|
sed -i '/^GITHUB_CLIENT_SECRET=/d' "$ENV_FILE"
|
||||||
|
|
||||||
@@ -72,8 +62,6 @@ cat >> "$ENV_FILE" << EOF
|
|||||||
# --- OAuth / SSO ---
|
# --- OAuth / SSO ---
|
||||||
GOOGLE_CLIENT_ID="$GOOGLE_CLIENT_ID"
|
GOOGLE_CLIENT_ID="$GOOGLE_CLIENT_ID"
|
||||||
GOOGLE_CLIENT_SECRET="$GOOGLE_CLIENT_SECRET"
|
GOOGLE_CLIENT_SECRET="$GOOGLE_CLIENT_SECRET"
|
||||||
APPLE_CLIENT_ID="$APPLE_CLIENT_ID"
|
|
||||||
APPLE_CLIENT_SECRET="$APPLE_CLIENT_SECRET"
|
|
||||||
GITHUB_CLIENT_ID="$GITHUB_CLIENT_ID"
|
GITHUB_CLIENT_ID="$GITHUB_CLIENT_ID"
|
||||||
GITHUB_CLIENT_SECRET="$GITHUB_CLIENT_SECRET"
|
GITHUB_CLIENT_SECRET="$GITHUB_CLIENT_SECRET"
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -8,19 +8,19 @@ import {
|
|||||||
} from "@/lib/oauth";
|
} from "@/lib/oauth";
|
||||||
import { signJWT } from "@/lib/auth";
|
import { signJWT } from "@/lib/auth";
|
||||||
|
|
||||||
const VALID_PROVIDERS = ["google", "apple", "github"];
|
const VALID_PROVIDERS = ["google", "github"];
|
||||||
|
|
||||||
async function handleCallback(
|
async function handleCallback(
|
||||||
req: NextRequest,
|
req: NextRequest,
|
||||||
provider: string
|
provider: string
|
||||||
): Promise<NextResponse> {
|
): Promise<NextResponse> {
|
||||||
try {
|
try {
|
||||||
// Grab code and state from query params or POST body (Apple uses form_post)
|
// Grab code and state from query params or POST body
|
||||||
const url = new URL(req.url);
|
const url = new URL(req.url);
|
||||||
let code = url.searchParams.get("code");
|
let code = url.searchParams.get("code");
|
||||||
let stateParam = url.searchParams.get("state");
|
let stateParam = url.searchParams.get("state");
|
||||||
|
|
||||||
// Apple may POST form data — try reading POST body
|
// Try reading POST body if code wasn't in query params
|
||||||
if (!code) {
|
if (!code) {
|
||||||
try {
|
try {
|
||||||
const contentType = req.headers.get("content-type") || "";
|
const contentType = req.headers.get("content-type") || "";
|
||||||
@@ -138,20 +138,3 @@ export async function GET(
|
|||||||
}
|
}
|
||||||
return handleCallback(req, provider);
|
return handleCallback(req, provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Apple OAuth may use form_post response_mode, which sends a POST. */
|
|
||||||
export async function POST(
|
|
||||||
req: NextRequest,
|
|
||||||
{ params }: { params: Promise<{ provider: string }> }
|
|
||||||
) {
|
|
||||||
const { provider } = await params;
|
|
||||||
if (!VALID_PROVIDERS.includes(provider)) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{
|
|
||||||
error: `Unsupported provider. Must be one of: ${VALID_PROVIDERS.join(", ")}`,
|
|
||||||
},
|
|
||||||
{ status: 400 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return handleCallback(req, provider);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
buildAuthorizationUrl,
|
buildAuthorizationUrl,
|
||||||
} from "@/lib/oauth";
|
} from "@/lib/oauth";
|
||||||
|
|
||||||
const VALID_PROVIDERS = ["google", "apple", "github"];
|
const VALID_PROVIDERS = ["google", "github"];
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
_req: NextRequest,
|
_req: NextRequest,
|
||||||
|
|||||||
@@ -32,15 +32,6 @@ function GoogleLogo({ className }: { className?: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Inline Apple SVG logo */
|
|
||||||
function AppleLogo({ className }: { className?: string }) {
|
|
||||||
return (
|
|
||||||
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Inline GitHub SVG logo */
|
/** Inline GitHub SVG logo */
|
||||||
function GitHubLogo({ className }: { className?: string }) {
|
function GitHubLogo({ className }: { className?: string }) {
|
||||||
return (
|
return (
|
||||||
@@ -141,18 +132,6 @@ export default function AuthPage() {
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Apple */}
|
|
||||||
<button
|
|
||||||
onClick={() => handleProviderClick("apple")}
|
|
||||||
disabled={authLoading}
|
|
||||||
className="w-full flex items-center gap-4 px-5 py-4 rounded-2xl bg-[#111118] border border-gray-800/60 active:bg-[#1a1a24] disabled:opacity-50 transition-all touch-manipulation"
|
|
||||||
>
|
|
||||||
<AppleLogo className="w-6 h-6 shrink-0 text-white" />
|
|
||||||
<span className="text-sm font-medium text-white">
|
|
||||||
Continue with Apple
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{/* GitHub */}
|
{/* GitHub */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleProviderClick("github")}
|
onClick={() => handleProviderClick("github")}
|
||||||
|
|||||||
+7
-92
@@ -3,7 +3,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
export default function SetupPage() {
|
export default function SetupPage() {
|
||||||
const [tab, setTab] = useState<'github' | 'google' | 'apple' | 'env'>('github');
|
const [tab, setTab] = useState<'github' | 'google' | 'env'>('github');
|
||||||
const [copied, setCopied] = useState('');
|
const [copied, setCopied] = useState('');
|
||||||
|
|
||||||
const copyToClipboard = (text: string, label: string) => {
|
const copyToClipboard = (text: string, label: string) => {
|
||||||
@@ -26,7 +26,6 @@ export default function SetupPage() {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ id: 'github' as const, label: 'GitHub', icon: '🐙' },
|
{ id: 'github' as const, label: 'GitHub', icon: '🐙' },
|
||||||
{ id: 'google' as const, label: 'Google', icon: '🔵' },
|
{ id: 'google' as const, label: 'Google', icon: '🔵' },
|
||||||
{ id: 'apple' as const, label: 'Apple', icon: '🍎' },
|
|
||||||
{ id: 'env' as const, label: 'Apply', icon: '⚡' },
|
{ id: 'env' as const, label: 'Apply', icon: '⚡' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ export default function SetupPage() {
|
|||||||
⚡ SSO Setup Guide
|
⚡ SSO Setup Guide
|
||||||
</h1>
|
</h1>
|
||||||
<p style={{ color: '#888', fontSize: '14px', marginBottom: '24px' }}>
|
<p style={{ color: '#888', fontSize: '14px', marginBottom: '24px' }}>
|
||||||
Create OAuth credentials for Google, Apple, and GitHub SSO.
|
Create OAuth credentials for Google and GitHub SSO.
|
||||||
Each takes ~5 minutes.
|
Each takes ~5 minutes.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -219,79 +218,7 @@ export default function SetupPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Apple Tab */}
|
|
||||||
{tab === 'apple' && (
|
|
||||||
<div>
|
|
||||||
<h2 style={{ fontSize: '18px', marginBottom: '16px', color: '#fff' }}>
|
|
||||||
Set Up Sign In with Apple
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<Step number={1} title="Go to Apple Developer">
|
|
||||||
<CodeBlock
|
|
||||||
text="https://developer.apple.com/account/resources/identifiers/list"
|
|
||||||
label="Apple URL"
|
|
||||||
copied={copied}
|
|
||||||
onCopy={copyToClipboard}
|
|
||||||
/>
|
|
||||||
<p style={{ color: '#ff6b6b', fontSize: '13px', marginTop: '8px' }}>
|
|
||||||
⚠️ Requires an Apple Developer account ($99/year).
|
|
||||||
</p>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step number={2} title="Register a Service ID">
|
|
||||||
<ul style={{ color: '#ccc', paddingLeft: '20px', lineHeight: 1.8 }}>
|
|
||||||
<li>Click <strong>"+"</strong> → <strong>"Service IDs"</strong></li>
|
|
||||||
<li>Description: <strong>FalahOS Auth</strong></li>
|
|
||||||
<li>Identifier: <strong>com.falahos.auth</strong></li>
|
|
||||||
<li>Click <strong>"Continue"</strong> → <strong>"Register"</strong></li>
|
|
||||||
</ul>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step number={3} title="Enable Sign in with Apple">
|
|
||||||
<ul style={{ color: '#ccc', paddingLeft: '20px', lineHeight: 1.8 }}>
|
|
||||||
<li>Click on your new Service ID</li>
|
|
||||||
<li>Check <strong>"Sign in with Apple"</strong> → <strong>"Configure"</strong></li>
|
|
||||||
<li>Select <strong>"Use as default"</strong> or create a new Service</li>
|
|
||||||
<li>Web Domain: <strong>falahos.my</strong></li>
|
|
||||||
<li>Return URLs: <strong>https://falahos.my/mobile/api/auth/apple/callback</strong></li>
|
|
||||||
<li>Click <strong>"Save"</strong></li>
|
|
||||||
</ul>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step number={4} title="Create a Key">
|
|
||||||
<ul style={{ color: '#ccc', paddingLeft: '20px', lineHeight: 1.8 }}>
|
|
||||||
<li>Go to <strong>Keys</strong> → <strong>"+"</strong></li>
|
|
||||||
<li>Select <strong>"Sign in with Apple"</strong></li>
|
|
||||||
<li>Configure with your Service ID</li>
|
|
||||||
<li>Download the <strong>.p8</strong> key file</li>
|
|
||||||
<li>Note the <strong>Key ID</strong></li>
|
|
||||||
</ul>
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step number={5} title="Copy Values">
|
|
||||||
<div style={{
|
|
||||||
background: '#0d0d0d',
|
|
||||||
border: '1px solid #333',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '12px',
|
|
||||||
marginTop: '8px',
|
|
||||||
}}>
|
|
||||||
<div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>
|
|
||||||
APPLE_CLIENT_ID=com.falahos.auth (your Service ID)
|
|
||||||
</div>
|
|
||||||
<div style={{ fontSize: '12px', color: '#888', marginBottom: '12px' }}>
|
|
||||||
APPLE_CLIENT_SECRET= (generated JWT, not needed immediately)
|
|
||||||
</div>
|
|
||||||
<CopyButton
|
|
||||||
text={`APPLE_CLIENT_ID="com.falahos.auth"\nAPPLE_CLIENT_SECRET=""`}
|
|
||||||
label="apple-template"
|
|
||||||
copied={copied}
|
|
||||||
onCopy={copyToClipboard}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Step>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Apply Tab */}
|
{/* Apply Tab */}
|
||||||
{tab === 'env' && (
|
{tab === 'env' && (
|
||||||
@@ -320,12 +247,11 @@ export default function SetupPage() {
|
|||||||
✅ Quick Summary
|
✅ Quick Summary
|
||||||
</h3>
|
</h3>
|
||||||
<p style={{ color: '#aaa', fontSize: '14px', lineHeight: 1.6 }}>
|
<p style={{ color: '#aaa', fontSize: '14px', lineHeight: 1.6 }}>
|
||||||
After creating all 3 OAuth apps, you'll have:
|
After creating both OAuth apps, you'll have:
|
||||||
</p>
|
</p>
|
||||||
<ul style={{ color: '#ccc', paddingLeft: '20px', lineHeight: 1.8 }}>
|
<ul style={{ color: '#ccc', paddingLeft: '20px', lineHeight: 1.8 }}>
|
||||||
<li>🐙 <strong>GitHub</strong>: Client ID + Secret</li>
|
<li>🐙 <strong>GitHub</strong>: Client ID + Secret</li>
|
||||||
<li>🔵 <strong>Google</strong>: Client ID + Secret</li>
|
<li>🔵 <strong>Google</strong>: Client ID + Secret</li>
|
||||||
<li>🍎 <strong>Apple</strong>: Service ID (Client ID) + Key</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<p style={{ color: '#aaa', fontSize: '14px', marginTop: '8px', lineHeight: 1.6 }}>
|
<p style={{ color: '#aaa', fontSize: '14px', marginTop: '8px', lineHeight: 1.6 }}>
|
||||||
Contact me on Telegram with the values and I'll apply them instantly.
|
Contact me on Telegram with the values and I'll apply them instantly.
|
||||||
@@ -473,14 +399,12 @@ function EnvForm() {
|
|||||||
const [ghSecret, setGhSecret] = useState('');
|
const [ghSecret, setGhSecret] = useState('');
|
||||||
const [glId, setGlId] = useState('');
|
const [glId, setGlId] = useState('');
|
||||||
const [glSecret, setGlSecret] = useState('');
|
const [glSecret, setGlSecret] = useState('');
|
||||||
const [apId, setApId] = useState('');
|
|
||||||
const [apSecret, setApSecret] = useState('');
|
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Build the env var text for copying
|
// Build the env var text for copying
|
||||||
const text = `# Paste these into your VPS:\ncd /root/falah-mobile\ncat >> .env << 'EOF'\nGOOGLE_CLIENT_ID="${glId}"\nGOOGLE_CLIENT_SECRET="${glSecret}"\nAPPLE_CLIENT_ID="${apId}"\nAPPLE_CLIENT_SECRET="${apSecret}"\nGITHUB_CLIENT_ID="${ghId}"\nGITHUB_CLIENT_SECRET="${ghSecret}"\nEOF\ndocker compose up -d --build`;
|
const text = `# Paste these into your VPS:\ncd /root/falah-mobile\ncat >> .env << 'EOF'\nGOOGLE_CLIENT_ID="${glId}"\nGOOGLE_CLIENT_SECRET="${glSecret}"\nGITHUB_CLIENT_ID="${ghId}"\nGITHUB_CLIENT_SECRET="${ghSecret}"\nEOF\ndocker compose up -d --build`;
|
||||||
navigator.clipboard.writeText(text).then(() => {
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
setTimeout(() => setSubmitted(false), 3000);
|
setTimeout(() => setSubmitted(false), 3000);
|
||||||
@@ -511,20 +435,11 @@ function EnvForm() {
|
|||||||
<InputField label="Client Secret" value={glSecret} onChange={setGlSecret} placeholder="GOCSPX-xxxxxxxx" secret />
|
<InputField label="Client Secret" value={glSecret} onChange={setGlSecret} placeholder="GOCSPX-xxxxxxxx" secret />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{
|
|
||||||
background: '#0d0d0d',
|
|
||||||
border: '1px solid #333',
|
|
||||||
borderRadius: '8px',
|
|
||||||
padding: '12px',
|
|
||||||
}}>
|
|
||||||
<h4 style={{ color: '#fff', fontSize: '14px', marginBottom: '8px' }}>🍎 Apple</h4>
|
|
||||||
<InputField label="Service ID (Client ID)" value={apId} onChange={setApId} placeholder="com.falahos.auth" />
|
|
||||||
<InputField label="Client Secret (JWT)" value={apSecret} onChange={setApSecret} placeholder="(generated JWT)" secret />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!ghId && !glId && !apId}
|
disabled={!ghId && !glId}
|
||||||
style={{
|
style={{
|
||||||
padding: '14px',
|
padding: '14px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
@@ -534,7 +449,7 @@ function EnvForm() {
|
|||||||
fontSize: '15px',
|
fontSize: '15px',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
opacity: (!ghId && !glId && !apId) ? 0.5 : 1,
|
opacity: (!ghId && !glId) ? 0.5 : 1,
|
||||||
transition: 'all 0.2s',
|
transition: 'all 0.2s',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
+1
-72
@@ -3,7 +3,7 @@ import { prisma } from "@/lib/prisma";
|
|||||||
|
|
||||||
// ─── Provider Config ────────────────────────────────────────────────────────
|
// ─── Provider Config ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export type Provider = "google" | "apple" | "github";
|
export type Provider = "google" | "github";
|
||||||
|
|
||||||
interface ProviderConfig {
|
interface ProviderConfig {
|
||||||
authorizeUrl: string;
|
authorizeUrl: string;
|
||||||
@@ -21,13 +21,6 @@ export const PROVIDER_CONFIGS: Record<Provider, ProviderConfig> = {
|
|||||||
clientIdEnv: "GOOGLE_CLIENT_ID",
|
clientIdEnv: "GOOGLE_CLIENT_ID",
|
||||||
clientSecretEnv: "GOOGLE_CLIENT_SECRET",
|
clientSecretEnv: "GOOGLE_CLIENT_SECRET",
|
||||||
},
|
},
|
||||||
apple: {
|
|
||||||
authorizeUrl: "https://appleid.apple.com/auth/authorize",
|
|
||||||
tokenUrl: "https://appleid.apple.com/auth/token",
|
|
||||||
scopes: ["name", "email"],
|
|
||||||
clientIdEnv: "APPLE_CLIENT_ID",
|
|
||||||
clientSecretEnv: "APPLE_CLIENT_SECRET",
|
|
||||||
},
|
|
||||||
github: {
|
github: {
|
||||||
authorizeUrl: "https://github.com/login/oauth/authorize",
|
authorizeUrl: "https://github.com/login/oauth/authorize",
|
||||||
tokenUrl: "https://github.com/login/oauth/access_token",
|
tokenUrl: "https://github.com/login/oauth/access_token",
|
||||||
@@ -137,63 +130,6 @@ export async function exchangeGoogleCode(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange an authorization code for an Apple access token,
|
|
||||||
* then decode the ID token to get user info.
|
|
||||||
*/
|
|
||||||
export async function exchangeAppleCode(
|
|
||||||
code: string,
|
|
||||||
redirectUri: string
|
|
||||||
): Promise<ProviderUser> {
|
|
||||||
const clientId = process.env.APPLE_CLIENT_ID;
|
|
||||||
const clientSecret = process.env.APPLE_CLIENT_SECRET;
|
|
||||||
|
|
||||||
if (!clientId || !clientSecret) {
|
|
||||||
throw new Error("Apple OAuth is not configured");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exchange code for token
|
|
||||||
const tokenRes = await fetch("https://appleid.apple.com/auth/token", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
||||||
body: new URLSearchParams({
|
|
||||||
code,
|
|
||||||
client_id: clientId,
|
|
||||||
client_secret: clientSecret,
|
|
||||||
redirect_uri: redirectUri,
|
|
||||||
grant_type: "authorization_code",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!tokenRes.ok) {
|
|
||||||
const errBody = await tokenRes.text();
|
|
||||||
throw new Error(`Apple token exchange failed: ${errBody}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenData = await tokenRes.json();
|
|
||||||
const idToken = tokenData.id_token;
|
|
||||||
|
|
||||||
if (!idToken) {
|
|
||||||
throw new Error("No ID token returned from Apple");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode the JWT payload (second segment) — no verification needed here
|
|
||||||
// since Apple's token endpoint already verified the auth code.
|
|
||||||
const payloadBase64 = idToken.split(".")[1];
|
|
||||||
const payloadJson = Buffer.from(payloadBase64, "base64").toString("utf-8");
|
|
||||||
const payload = JSON.parse(payloadJson);
|
|
||||||
|
|
||||||
return {
|
|
||||||
providerId: payload.sub,
|
|
||||||
email: payload.email || "",
|
|
||||||
name:
|
|
||||||
payload.name ||
|
|
||||||
`${payload.given_name || ""} ${payload.family_name || ""}`.trim() ||
|
|
||||||
"User",
|
|
||||||
avatar: null, // Apple does not provide a profile photo
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchange an authorization code for a GitHub access token,
|
* Exchange an authorization code for a GitHub access token,
|
||||||
* then fetch user info (and primary email).
|
* then fetch user info (and primary email).
|
||||||
@@ -292,8 +228,6 @@ export async function exchangeCode(
|
|||||||
switch (provider) {
|
switch (provider) {
|
||||||
case "google":
|
case "google":
|
||||||
return exchangeGoogleCode(code, redirectUri);
|
return exchangeGoogleCode(code, redirectUri);
|
||||||
case "apple":
|
|
||||||
return exchangeAppleCode(code, redirectUri);
|
|
||||||
case "github":
|
case "github":
|
||||||
return exchangeGitHubCode(code, redirectUri);
|
return exchangeGitHubCode(code, redirectUri);
|
||||||
default:
|
default:
|
||||||
@@ -381,10 +315,5 @@ export function buildAuthorizationUrl(
|
|||||||
state,
|
state,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apple requires response_mode=form_post
|
|
||||||
if (provider === "apple") {
|
|
||||||
params.set("response_mode", "form_post");
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${config.authorizeUrl}?${params.toString()}`;
|
return `${config.authorizeUrl}?${params.toString()}`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user