Skip to content

Commit c57fa34

Browse files
committed
cli auth
1 parent 491ad34 commit c57fa34

File tree

3 files changed

+231
-11
lines changed

3 files changed

+231
-11
lines changed

src/pages/Auth/Auth.tsx

Lines changed: 102 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, {useCallback, useEffect, useState} from 'react';
22
import './Auth.css';
33
import {useAuth} from '../../contexts/AuthContext';
4+
import {auth} from '../../config/firebase';
45

56
const CLIENT_ID = '87955960620-dv9h8pfv4a97mno598dcc3m1nlt0h6u4.apps.googleusercontent.com';
67

@@ -21,20 +22,77 @@ declare global {
2122

2223
const Auth: React.FC = () => {
2324
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false);
24-
const {login, loginWithApple} = useAuth();
25+
const {user, login, loginWithApple} = useAuth();
26+
const [isGenerating, setIsGenerating] = useState(false);
27+
const [isRedirecting, setIsRedirecting] = useState(false);
28+
29+
// Get redirect URL from URL params (for CLI integration)
30+
const urlParams = new URLSearchParams(window.location.search);
31+
const redirectUrl = urlParams.get('redirect_url');
32+
33+
const generateTokenAndRedirect = async () => {
34+
if (!redirectUrl || !user) return;
35+
36+
setIsGenerating(true);
37+
try {
38+
// Get the current user's ID token
39+
const idToken = await auth.currentUser?.getIdToken();
40+
if (!idToken) {
41+
throw new Error('Failed to get authentication token');
42+
}
43+
44+
// Call the salamander-service to generate custom token
45+
const response = await fetch('https://api.salamander.space/v1/generate-custom-token', {
46+
method: 'POST',
47+
headers: {
48+
'Content-Type': 'application/json',
49+
},
50+
body: JSON.stringify({
51+
firebaseIdToken: idToken
52+
}),
53+
});
54+
55+
if (!response.ok) {
56+
const errorData = await response.json();
57+
throw new Error(errorData.error || 'Failed to generate custom token');
58+
}
59+
60+
const result = await response.json();
61+
62+
setIsRedirecting(true);
63+
// Redirect to the specified URL with the token
64+
window.location.href = `${redirectUrl}?token=${encodeURIComponent(result.customToken)}`;
65+
} catch (err: any) {
66+
console.error('Error generating custom token:', err);
67+
alert(err.message || 'Failed to generate authentication token');
68+
} finally {
69+
setIsGenerating(false);
70+
}
71+
};
72+
73+
useEffect(() => {
74+
// If user is signed in and we have a redirect URL, generate token and redirect
75+
if (user && redirectUrl && !isGenerating && !isRedirecting) {
76+
generateTokenAndRedirect();
77+
}
78+
// eslint-disable-next-line react-hooks/exhaustive-deps
79+
}, [user, redirectUrl, isGenerating, isRedirecting]);
2580

2681
const handleCredentialResponse = useCallback(async (response: any) => {
2782
console.log('Encoded JWT ID token: ' + response.credential);
2883

2984
try {
3085
await login(response.credential);
31-
// Redirect to account page after successful login
32-
window.location.hash = 'account';
86+
// If we have a redirect URL, the useEffect will handle the redirect
87+
if (!redirectUrl) {
88+
// Redirect to account page after successful login
89+
window.location.hash = 'account';
90+
}
3391
} catch (error) {
3492
console.error('Failed to process login:', error);
3593
alert(error instanceof Error ? error.message : 'Sign-in failed. Please try again.');
3694
}
37-
}, [login]);
95+
}, [login, redirectUrl]);
3896

3997
useEffect(() => {
4098
const initializeGoogle = () => {
@@ -71,14 +129,52 @@ const Auth: React.FC = () => {
71129
const handleAppleAuth = async () => {
72130
try {
73131
await loginWithApple();
74-
// Redirect to account page after successful login
75-
window.location.hash = 'account';
132+
// If we have a redirect URL, the useEffect will handle the redirect
133+
if (!redirectUrl) {
134+
// Redirect to account page after successful login
135+
window.location.hash = 'account';
136+
}
76137
} catch (error) {
77138
console.error('Apple sign-in failed:', error);
78139
alert(error instanceof Error ? error.message : 'Apple sign-in failed. Please try again.');
79140
}
80141
};
81142

143+
// Show loading state if generating token or redirecting
144+
if (isGenerating || isRedirecting) {
145+
return (
146+
<div className="auth-container">
147+
<div className="auth-card">
148+
<div className="auth-header">
149+
<img src="images/logo_salamander.png" alt="Salamander" className="auth-logo"/>
150+
<h1 className="auth-title">Salamander</h1>
151+
<p className="auth-tagline">Never be AFK</p>
152+
</div>
153+
154+
<div className="auth-content" style={{ textAlign: 'center' }}>
155+
<div style={{
156+
width: '32px',
157+
height: '32px',
158+
border: '3px solid #374151',
159+
borderTop: '3px solid #ff6b35',
160+
borderRadius: '50%',
161+
animation: 'spin 1s linear infinite',
162+
margin: '0 auto 16px'
163+
}}></div>
164+
<p style={{ color: '#d1d5db', fontSize: '14px' }}>
165+
{isRedirecting ? 'Redirecting to CLI...' : 'Generating authentication token...'}
166+
</p>
167+
{isRedirecting && (
168+
<p style={{ color: '#9ca3af', fontSize: '12px', marginTop: '16px', fontStyle: 'italic' }}>
169+
If the redirect doesn't work, you can close this window and return to your terminal.
170+
</p>
171+
)}
172+
</div>
173+
</div>
174+
</div>
175+
);
176+
}
177+
82178
return (
83179
<div className="auth-container">
84180
<div className="auth-card">

src/pages/CliAuth/CliAuth.css

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,44 @@
9292
font-size: 18px;
9393
}
9494

95+
.google-login-btn {
96+
background: white;
97+
color: #1f2937;
98+
border: 1px solid #d1d5db;
99+
border-radius: 8px;
100+
padding: 12px 24px;
101+
font-size: 16px;
102+
font-weight: 500;
103+
cursor: pointer;
104+
display: flex;
105+
align-items: center;
106+
justify-content: center;
107+
margin: 0 auto;
108+
transition: all 0.2s;
109+
font-family: inherit;
110+
}
111+
112+
.google-login-btn:hover {
113+
background: #f9fafb;
114+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
115+
}
116+
117+
.google-login-btn:disabled {
118+
opacity: 0.6;
119+
cursor: not-allowed;
120+
}
121+
122+
.google-auth-content {
123+
display: flex;
124+
align-items: center;
125+
gap: 8px;
126+
}
127+
128+
.google-icon {
129+
width: 18px;
130+
height: 18px;
131+
}
132+
95133
.auth-message {
96134
color: #d1d5db;
97135
font-size: 14px;

src/pages/CliAuth/CliAuth.tsx

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
1-
import React, { useState, useEffect } from 'react';
1+
import React, { useState, useEffect, useCallback } from 'react';
22
import { useAuth } from '../../contexts/AuthContext';
33
import { auth } from '../../config/firebase';
44
import './CliAuth.css';
55

6+
const CLIENT_ID = '87955960620-dv9h8pfv4a97mno598dcc3m1nlt0h6u4.apps.googleusercontent.com';
7+
8+
declare global {
9+
interface Window {
10+
google: {
11+
accounts: {
12+
id: {
13+
initialize: (config: any) => void;
14+
prompt: () => void;
15+
renderButton: (element: HTMLElement, config: any) => void;
16+
};
17+
};
18+
};
19+
}
20+
}
21+
622
interface GenerateCustomTokenResponse {
723
customToken: string;
824
uid: string;
@@ -11,22 +27,56 @@ interface GenerateCustomTokenResponse {
1127
}
1228

1329
const CliAuth: React.FC = () => {
14-
const { user, loginWithApple, isLoading } = useAuth();
30+
const { user, login, loginWithApple, isLoading } = useAuth();
1531
const [customToken, setCustomToken] = useState<string | null>(null);
1632
const [isGenerating, setIsGenerating] = useState(false);
1733
const [error, setError] = useState<string | null>(null);
1834
const [isRedirecting, setIsRedirecting] = useState(false);
35+
const [isGoogleLoaded, setIsGoogleLoaded] = useState(false);
1936

2037
// Get auth type and callback URL from URL params
2138
const urlParams = new URLSearchParams(window.location.search);
2239
const authType = urlParams.get('type') || 'apple';
2340
const callbackUrl = urlParams.get('callback');
2441

42+
const handleCredentialResponse = useCallback(async (response: any) => {
43+
console.log('Encoded JWT ID token: ' + response.credential);
44+
45+
try {
46+
await login(response.credential);
47+
// generateTokenAndRedirect will be called automatically via useEffect when user state updates
48+
} catch (error) {
49+
console.error('Failed to process login:', error);
50+
setError(error instanceof Error ? error.message : 'Google sign-in failed. Please try again.');
51+
}
52+
}, [login]);
53+
54+
useEffect(() => {
55+
// Initialize Google Sign-In if needed
56+
if (authType === 'google') {
57+
const initializeGoogle = () => {
58+
if (window.google && window.google.accounts) {
59+
window.google.accounts.id.initialize({
60+
client_id: CLIENT_ID,
61+
callback: handleCredentialResponse,
62+
});
63+
setIsGoogleLoaded(true);
64+
} else {
65+
// Retry after a short delay if Google hasn't loaded yet
66+
setTimeout(initializeGoogle, 100);
67+
}
68+
};
69+
70+
initializeGoogle();
71+
}
72+
}, [authType, handleCredentialResponse]);
73+
2574
useEffect(() => {
2675
// If user is already signed in and we have a callback URL, generate custom token and redirect
2776
if (user && callbackUrl && !customToken && !isGenerating) {
2877
generateTokenAndRedirect();
2978
}
79+
// eslint-disable-next-line react-hooks/exhaustive-deps
3080
}, [user, callbackUrl, customToken, isGenerating]);
3181

3282
const generateToken = async (): Promise<string | null> => {
@@ -92,6 +142,22 @@ const CliAuth: React.FC = () => {
92142
}
93143
};
94144

145+
const handleGoogleLogin = async () => {
146+
if (!isGoogleLoaded) {
147+
setError('Google Sign-In not loaded. Please try again in a moment.');
148+
return;
149+
}
150+
151+
setError(null);
152+
try {
153+
// Trigger the One Tap flow
154+
window.google.accounts.id.prompt();
155+
} catch (error) {
156+
console.error('Google sign-in failed:', error);
157+
setError('Google sign-in failed. Please try again.');
158+
}
159+
};
160+
95161

96162
if (isLoading) {
97163
return (
@@ -110,7 +176,7 @@ const CliAuth: React.FC = () => {
110176
<div className="cli-auth-header">
111177
<div className="cli-auth-logo">S</div>
112178
<h1>Salamander CLI Authentication</h1>
113-
<p>Authenticate your CLI with {authType === 'apple' ? 'Apple ID' : 'Google'}</p>
179+
<p>Authenticate your CLI with {authType === 'apple' ? 'Apple ID' : authType === 'google' ? 'Google' : authType}</p>
114180
</div>
115181

116182
{!user && (
@@ -124,10 +190,30 @@ const CliAuth: React.FC = () => {
124190
<span className="apple-icon">🍎</span>
125191
Sign in with Apple
126192
</button>
193+
) : authType === 'google' ? (
194+
<button
195+
className="google-login-btn"
196+
onClick={handleGoogleLogin}
197+
disabled={isLoading || !isGoogleLoaded}
198+
>
199+
<div className="google-auth-content">
200+
<svg className="google-icon" viewBox="0 0 24 24" width="18" height="18">
201+
<path fill="#4285F4"
202+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
203+
<path fill="#34A853"
204+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
205+
<path fill="#FBBC05"
206+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
207+
<path fill="#EA4335"
208+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
209+
</svg>
210+
<span>Sign in with Google</span>
211+
</div>
212+
</button>
127213
) : (
128214
<div className="auth-message">
129-
<p>Google authentication is not yet supported in this CLI auth flow.</p>
130-
<p>Please use the standard Google OAuth flow in your CLI.</p>
215+
<p>Unknown authentication type: {authType}</p>
216+
<p>Supported types: apple, google</p>
131217
</div>
132218
)}
133219
</div>

0 commit comments

Comments
 (0)