File size: 6,444 Bytes
f7686fe
8134b4e
 
f7686fe
 
698ffee
f7686fe
 
5eda54b
f7686fe
 
8134b4e
f7686fe
698ffee
f7686fe
 
 
 
 
698ffee
 
f7686fe
 
 
 
e9a187e
 
 
f7686fe
 
 
 
 
 
 
 
 
 
 
 
 
 
698ffee
 
 
 
f7686fe
 
 
 
 
 
 
 
 
 
698ffee
f7686fe
e9a187e
 
 
f7686fe
 
 
 
 
698ffee
 
 
 
 
f7686fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8134b4e
 
f7686fe
698ffee
8134b4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f7686fe
 
 
 
 
 
 
 
698ffee
f7686fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698ffee
f7686fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import React, { useCallback, useEffect, useState } from 'react';
import { Link, useLocation, useSearchParams } from 'react-router-dom';
import { LogOut, Settings } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { apiFetch } from '@/lib/api';

/**
 * Sign-in (Google). Workspace details and switching context live in Settings / session. OAuth callback sets session.
 */
export default function GoogleAuthBar() {
    const location = useLocation();
    const [searchParams, setSearchParams] = useSearchParams();
    const [phase, setPhase] = useState('loading');
    const [googleOn, setGoogleOn] = useState(false);
    const [user, setUser] = useState(null);
    const refresh = useCallback(async () => {
        try {
            const [st, me] = await Promise.all([
                apiFetch('/api/auth/status').then((r) => r.json()),
                apiFetch('/api/auth/me').then((r) => (r.ok ? r.json() : null)),
            ]);
            setGoogleOn(!!st.googleConfigured);
            setUser(me);
            setPhase('ready');
            if (typeof window !== 'undefined') {
                window.dispatchEvent(new CustomEvent('emailout-auth-changed'));
            }
        } catch {
            setPhase('error');
        }
    }, []);

    useEffect(() => {
        refresh();
    }, [refresh]);

    useEffect(() => {
        const err = searchParams.get('auth_error');
        if (!err) return;
        if (err === 'access_denied') {
            console.info('Google sign-in was cancelled.');
        } else if (err === 'invite_email_mismatch') {
            console.warn(
                'Invitation email does not match your Google account. Sign in with the invited address.'
            );
        } else {
            console.warn('Google sign-in error:', err);
        }
        const next = new URLSearchParams(searchParams);
        next.delete('auth_error');
        setSearchParams(next, { replace: true });
    }, [searchParams, setSearchParams]);

    const logout = async () => {
        try {
            await apiFetch('/api/auth/logout', { method: 'POST' });
            setUser(null);
            if (typeof window !== 'undefined') {
                window.dispatchEvent(new CustomEvent('emailout-auth-changed'));
            }
        } catch (e) {
            console.error(e);
        }
    };

    const inviteParam = searchParams.get('invite');
    const googleHref = inviteParam
        ? `/api/auth/google?invite=${encodeURIComponent(inviteParam)}`
        : '/api/auth/google';

    if (phase === 'loading' || (phase === 'ready' && !googleOn)) {
        if (phase === 'loading') {
            return (
                <div
                    className="h-9 w-24 shrink-0 rounded-md bg-slate-100/80 animate-pulse"
                    aria-hidden
                />
            );
        }
        return null;
    }

    if (phase === 'error') {
        return null;
    }

    if (user) {
        const settingsActive =
            location.pathname === '/settings' || location.pathname.startsWith('/settings/');
        return (
            <div className="flex max-w-[min(100vw-6rem,28rem)] flex-wrap items-center justify-end gap-2">
                <Button
                    asChild
                    variant="ghost"
                    size="icon"
                    className={cn(
                        'h-9 w-9 shrink-0 text-slate-600 hover:text-violet-700 hover:bg-violet-50',
                        settingsActive && 'text-violet-700 bg-violet-50'
                    )}
                    title="Settings"
                    aria-current={settingsActive ? 'page' : undefined}
                >
                    <Link to="/settings">
                        <Settings className="h-5 w-5" aria-hidden />
                        <span className="sr-only">Settings</span>
                    </Link>
                </Button>
                {user.picture ? (
                    <img
                        src={user.picture}
                        alt=""
                        className="h-8 w-8 shrink-0 rounded-full border border-slate-200 object-cover"
                        referrerPolicy="no-referrer"
                    />
                ) : null}
                <span className="hidden min-w-0 truncate text-sm text-slate-600 lg:inline">
                    {user.name || user.email || 'Signed in'}
                </span>
                <Button
                    type="button"
                    variant="outline"
                    size="sm"
                    className="shrink-0 gap-1"
                    onClick={logout}
                    title="Sign out"
                >
                    <LogOut className="h-3.5 w-3.5" />
                    <span className="hidden sm:inline">Sign out</span>
                </Button>
            </div>
        );
    }

    return (
        <Button
            asChild
            variant="outline"
            size="sm"
            className={cn(
                'shrink-0 gap-2 border-slate-200 bg-white text-slate-800 shadow-sm',
                'hover:bg-slate-50'
            )}
        >
            <a href={googleHref} className="inline-flex items-center gap-2">
                <GoogleMark className="h-4 w-4 shrink-0" />
                <span className="whitespace-nowrap">Sign in with Google</span>
            </a>
        </Button>
    );
}

function GoogleMark({ className }) {
    return (
        <svg className={className} viewBox="0 0 24 24" aria-hidden>
            <path
                fill="#4285F4"
                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"
            />
            <path
                fill="#34A853"
                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"
            />
            <path
                fill="#FBBC05"
                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"
            />
            <path
                fill="#EA4335"
                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"
            />
        </svg>
    );
}