deevoauth 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,202 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, Suspense } from 'react';
4
+ import { useSearchParams, useRouter } from 'next/navigation';
5
+ import { AuthProvider, useAuth } from '@/lib/auth-context';
6
+
7
+ function ConsentContent() {
8
+ const searchParams = useSearchParams();
9
+ const router = useRouter();
10
+ const { user, loading } = useAuth();
11
+
12
+ const [processing, setProcessing] = useState(false);
13
+ const [error, setError] = useState('');
14
+ const [appName, setAppName] = useState('');
15
+
16
+ const clientId = searchParams.get('client_id');
17
+ const redirectUri = searchParams.get('redirect_uri');
18
+ const scope = searchParams.get('scope') || 'profile email';
19
+
20
+ useEffect(() => {
21
+ if (!loading && !user) {
22
+ // Redirect to login with OAuth params
23
+ const params = new URLSearchParams({
24
+ client_id: clientId || '',
25
+ redirect_uri: redirectUri || '',
26
+ });
27
+ router.push(`/login?${params.toString()}`);
28
+ }
29
+ }, [user, loading, router, clientId, redirectUri]);
30
+
31
+ useEffect(() => {
32
+ // Fetch app name from client_id
33
+ if (clientId) {
34
+ setAppName(clientId); // Placeholder — could fetch from Firestore
35
+ }
36
+ }, [clientId]);
37
+
38
+ const handleAllow = async () => {
39
+ if (!clientId || !redirectUri) {
40
+ setError('Missing application details.');
41
+ return;
42
+ }
43
+
44
+ setProcessing(true);
45
+ try {
46
+ const idToken = await user.getIdToken();
47
+ const response = await fetch('/api/internal/generate-code', {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({ idToken, clientId, redirectUri }),
51
+ });
52
+
53
+ const data = await response.json();
54
+ if (data.error) throw new Error(data.error);
55
+
56
+ window.location.href = `${redirectUri}?code=${data.code}`;
57
+ } catch (err) {
58
+ console.error(err);
59
+ setError('Failed to authorize. Please try again.');
60
+ setProcessing(false);
61
+ }
62
+ };
63
+
64
+ const handleDeny = () => {
65
+ if (redirectUri) {
66
+ window.location.href = `${redirectUri}?error=access_denied`;
67
+ } else {
68
+ router.push('/dashboard');
69
+ }
70
+ };
71
+
72
+ if (loading || !user) {
73
+ return (
74
+ <div className="loading-overlay">
75
+ <img src="/deevo-logo.svg" alt="Deevo" style={{ height: 32, width: 'auto' }} />
76
+ <span className="spinner" style={{ width: 24, height: 24, color: 'var(--primary)', marginTop: 'var(--space-4)' }} />
77
+ </div>
78
+ );
79
+ }
80
+
81
+ const permissions = scope.split(' ').map((s) => {
82
+ switch (s) {
83
+ case 'profile': return { label: 'View your profile information', desc: 'Name, avatar' };
84
+ case 'email': return { label: 'View your email address', desc: 'Email' };
85
+ case 'openid': return { label: 'Verify your identity', desc: 'User ID' };
86
+ default: return { label: s, desc: '' };
87
+ }
88
+ });
89
+
90
+ return (
91
+ <>
92
+ <div className="bg-animated" />
93
+ <div className="page-center">
94
+ <div className="auth-container" style={{ maxWidth: 480 }}>
95
+ <div className="glass-card consent-card">
96
+ {/* App Icon */}
97
+ <div className="consent-app-icon">
98
+ <span>{(appName || '?')[0].toUpperCase()}</span>
99
+ </div>
100
+
101
+ <h1 style={{ fontSize: 'var(--font-size-xl)', fontWeight: 600, marginBottom: 'var(--space-2)' }}>
102
+ Authorize Access
103
+ </h1>
104
+ <p style={{ color: 'var(--on-surface-variant)', fontSize: 'var(--font-size-sm)' }}>
105
+ <strong style={{ color: 'var(--on-surface)' }}>{appName || 'An application'}</strong> wants to access your Deevo Account
106
+ </p>
107
+
108
+ {/* User info */}
109
+ <div style={{
110
+ display: 'flex',
111
+ alignItems: 'center',
112
+ gap: 'var(--space-3)',
113
+ padding: 'var(--space-4)',
114
+ background: 'var(--surface-container-low)',
115
+ borderRadius: 'var(--radius-md)',
116
+ margin: 'var(--space-6) 0',
117
+ }}>
118
+ <div className="avatar" style={{ width: 36, height: 36, cursor: 'default' }}>
119
+ {user.photoURL ? (
120
+ <img src={user.photoURL} alt="" referrerPolicy="no-referrer" />
121
+ ) : (
122
+ <div className="avatar-placeholder" style={{ fontSize: 'var(--font-size-sm)' }}>
123
+ {(user.displayName || user.email || '?')[0].toUpperCase()}
124
+ </div>
125
+ )}
126
+ </div>
127
+ <div>
128
+ <div style={{ fontWeight: 500, fontSize: 'var(--font-size-sm)' }}>
129
+ {user.displayName || 'Deevo User'}
130
+ </div>
131
+ <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--on-surface-variant)' }}>
132
+ {user.email}
133
+ </div>
134
+ </div>
135
+ </div>
136
+
137
+ {/* Permissions */}
138
+ <div className="consent-permissions">
139
+ <p style={{ fontSize: 'var(--font-size-xs)', color: 'var(--outline)', marginBottom: 'var(--space-3)', textTransform: 'uppercase', fontWeight: 600, letterSpacing: '0.05em' }}>
140
+ This will allow the app to:
141
+ </p>
142
+ {permissions.map((perm, i) => (
143
+ <div key={i} className="consent-permission-item">
144
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
145
+ <polyline points="20 6 9 17 4 12" />
146
+ </svg>
147
+ <span>{perm.label}</span>
148
+ </div>
149
+ ))}
150
+ </div>
151
+
152
+ {/* Error */}
153
+ {error && (
154
+ <div className="alert alert-error" style={{ marginBottom: 'var(--space-4)' }}>
155
+ <span>{error}</span>
156
+ </div>
157
+ )}
158
+
159
+ {/* Actions */}
160
+ <div className="consent-actions">
161
+ <button className="btn btn-secondary" onClick={handleDeny} disabled={processing}>
162
+ Deny
163
+ </button>
164
+ <button className="btn btn-primary" onClick={handleAllow} disabled={processing}>
165
+ {processing ? (
166
+ <>
167
+ <span className="spinner" />
168
+ Authorizing...
169
+ </>
170
+ ) : (
171
+ 'Allow'
172
+ )}
173
+ </button>
174
+ </div>
175
+ </div>
176
+ </div>
177
+ </div>
178
+ </>
179
+ );
180
+ }
181
+
182
+ function ConsentFallback() {
183
+ return (
184
+ <div className="page-center">
185
+ <div className="auth-container">
186
+ <div className="glass-card auth-card" style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-16)' }}>
187
+ <span className="spinner" style={{ width: 32, height: 32 }} />
188
+ </div>
189
+ </div>
190
+ </div>
191
+ );
192
+ }
193
+
194
+ export default function ConsentPage() {
195
+ return (
196
+ <AuthProvider>
197
+ <Suspense fallback={<ConsentFallback />}>
198
+ <ConsentContent />
199
+ </Suspense>
200
+ </AuthProvider>
201
+ );
202
+ }
@@ -0,0 +1,254 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { AuthProvider, useAuth } from '@/lib/auth-context';
7
+
8
+ function DashboardContent() {
9
+ const router = useRouter();
10
+ const { user, loading, signOut } = useAuth();
11
+ const [menuOpen, setMenuOpen] = useState(false);
12
+ const [editing, setEditing] = useState(false);
13
+ const [fullName, setFullName] = useState('');
14
+ const [saving, setSaving] = useState(false);
15
+ const [connectedApps, setConnectedApps] = useState([]);
16
+
17
+ useEffect(() => {
18
+ if (!loading && !user) {
19
+ router.push('/login');
20
+ }
21
+ }, [user, loading, router]);
22
+
23
+ useEffect(() => {
24
+ if (user) {
25
+ setFullName(user.displayName || '');
26
+ }
27
+ }, [user]);
28
+
29
+ const handleSignOut = async () => {
30
+ await signOut();
31
+ router.push('/');
32
+ };
33
+
34
+ const handleSaveProfile = async () => {
35
+ setSaving(true);
36
+ try {
37
+ const idToken = await user.getIdToken();
38
+ await fetch('/api/internal/create-user', {
39
+ method: 'POST',
40
+ headers: { 'Content-Type': 'application/json' },
41
+ body: JSON.stringify({ idToken, fullName }),
42
+ });
43
+ setEditing(false);
44
+ } catch (err) {
45
+ console.error('Failed to save profile:', err);
46
+ }
47
+ setSaving(false);
48
+ };
49
+
50
+ if (loading || !user) {
51
+ return (
52
+ <div className="loading-overlay">
53
+ <img src="/deevo-logo.svg" alt="Deevo" style={{ height: 32, width: 'auto' }} />
54
+ <span className="spinner" style={{ width: 24, height: 24, color: 'var(--primary)', marginTop: 'var(--space-4)' }} />
55
+ </div>
56
+ );
57
+ }
58
+
59
+ const initials = (user.displayName || user.email || '?')
60
+ .split(' ')
61
+ .map((n) => n[0])
62
+ .join('')
63
+ .toUpperCase()
64
+ .slice(0, 2);
65
+
66
+ return (
67
+ <>
68
+ <div className="bg-animated" />
69
+
70
+ {/* Navbar */}
71
+ <nav className="navbar">
72
+ <Link href="/" className="navbar-brand">
73
+ <img src="/deevo-logo.svg" alt="Deevo" style={{ height: 22, width: 'auto' }} />
74
+ </Link>
75
+
76
+ <div className="navbar-actions">
77
+ <div className="user-menu">
78
+ <div
79
+ className="avatar"
80
+ onClick={() => setMenuOpen(!menuOpen)}
81
+ >
82
+ {user.photoURL ? (
83
+ <img src={user.photoURL} alt="Avatar" referrerPolicy="no-referrer" />
84
+ ) : (
85
+ <div className="avatar-placeholder">{initials}</div>
86
+ )}
87
+ </div>
88
+
89
+ <div className={`user-menu-dropdown ${menuOpen ? 'open' : ''}`}>
90
+ <div style={{ padding: 'var(--space-3) var(--space-4)' }}>
91
+ <div style={{ fontWeight: 500, fontSize: 'var(--font-size-sm)' }}>
92
+ {user.displayName || 'Deevo User'}
93
+ </div>
94
+ <div style={{ fontSize: 'var(--font-size-xs)', color: 'var(--on-surface-variant)' }}>
95
+ {user.email}
96
+ </div>
97
+ </div>
98
+ <div className="user-menu-divider" />
99
+ <button className="user-menu-item" onClick={() => { setMenuOpen(false); }}>
100
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
101
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
102
+ <circle cx="12" cy="7" r="4" />
103
+ </svg>
104
+ Profile
105
+ </button>
106
+ <div className="user-menu-divider" />
107
+ <button className="user-menu-item" onClick={handleSignOut} style={{ color: 'var(--error)' }}>
108
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
109
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
110
+ <polyline points="16 17 21 12 16 7" />
111
+ <line x1="21" y1="12" x2="9" y2="12" />
112
+ </svg>
113
+ Sign Out
114
+ </button>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </nav>
119
+
120
+ {/* Dashboard Content */}
121
+ <div className="dashboard-grid" style={{ paddingTop: 'var(--space-8)' }}>
122
+ {/* Profile Section */}
123
+ <div className="glass-card section-card">
124
+ <h2 className="section-title">Profile</h2>
125
+ <div className="profile-header">
126
+ <div className="avatar avatar-lg">
127
+ {user.photoURL ? (
128
+ <img src={user.photoURL} alt="Avatar" referrerPolicy="no-referrer" />
129
+ ) : (
130
+ <div className="avatar-placeholder">{initials}</div>
131
+ )}
132
+ </div>
133
+ <div className="profile-info">
134
+ {editing ? (
135
+ <div style={{ display: 'flex', gap: 'var(--space-3)', alignItems: 'center' }}>
136
+ <input
137
+ className="form-input"
138
+ value={fullName}
139
+ onChange={(e) => setFullName(e.target.value)}
140
+ style={{ maxWidth: 250 }}
141
+ placeholder="Your name"
142
+ />
143
+ <button className="btn btn-primary" onClick={handleSaveProfile} disabled={saving} style={{ padding: '0.5rem 1rem', fontSize: 'var(--font-size-sm)' }}>
144
+ {saving ? 'Saving...' : 'Save'}
145
+ </button>
146
+ <button className="btn btn-ghost" onClick={() => { setEditing(false); setFullName(user.displayName || ''); }} style={{ padding: '0.5rem 1rem', fontSize: 'var(--font-size-sm)' }}>
147
+ Cancel
148
+ </button>
149
+ </div>
150
+ ) : (
151
+ <>
152
+ <h2>{user.displayName || 'Deevo User'}</h2>
153
+ <p>{user.email}</p>
154
+ <div style={{ display: 'flex', gap: 'var(--space-2)', marginTop: 'var(--space-2)' }}>
155
+ {user.emailVerified ? (
156
+ <span className="chip chip-success">✓ Verified</span>
157
+ ) : (
158
+ <span className="chip chip-warning">⚠ Unverified</span>
159
+ )}
160
+ </div>
161
+ </>
162
+ )}
163
+ </div>
164
+ </div>
165
+ {!editing && (
166
+ <button className="btn btn-secondary" onClick={() => setEditing(true)} style={{ fontSize: 'var(--font-size-sm)', padding: '0.5rem 1rem' }}>
167
+ Edit Profile
168
+ </button>
169
+ )}
170
+ </div>
171
+
172
+ {/* Connected Apps */}
173
+ <div className="glass-card section-card">
174
+ <h2 className="section-title">Connected Apps</h2>
175
+ {connectedApps.length === 0 ? (
176
+ <div style={{ textAlign: 'center', padding: 'var(--space-8) 0', color: 'var(--on-surface-variant)' }}>
177
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ margin: '0 auto var(--space-4)', opacity: 0.3 }}>
178
+ <rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
179
+ <line x1="8" y1="21" x2="16" y2="21" />
180
+ <line x1="12" y1="17" x2="12" y2="21" />
181
+ </svg>
182
+ <p style={{ fontSize: 'var(--font-size-sm)' }}>No apps connected yet</p>
183
+ <p style={{ fontSize: 'var(--font-size-xs)', marginTop: 'var(--space-1)', opacity: 0.6 }}>
184
+ When you sign in to apps using Deevo, they&apos;ll appear here.
185
+ </p>
186
+ </div>
187
+ ) : (
188
+ <div className="app-list">
189
+ {connectedApps.map((app, i) => (
190
+ <div key={i} className="app-item">
191
+ <div className="app-item-info">
192
+ <span className="app-item-name">{app.name}</span>
193
+ <span className="app-item-scope">{app.scope}</span>
194
+ </div>
195
+ <button className="btn btn-ghost" style={{ fontSize: 'var(--font-size-xs)', color: 'var(--error)' }}>
196
+ Revoke
197
+ </button>
198
+ </div>
199
+ ))}
200
+ </div>
201
+ )}
202
+ </div>
203
+
204
+ {/* Account Security */}
205
+ <div className="glass-card section-card">
206
+ <h2 className="section-title">Account Security</h2>
207
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-3)' }}>
208
+ <div className="app-item">
209
+ <div className="app-item-info">
210
+ <span className="app-item-name">Sign-in Method</span>
211
+ <span className="app-item-scope">
212
+ {user.providerData.map((p) => p.providerId === 'google.com' ? 'Google' : 'Email/Password').join(', ')}
213
+ </span>
214
+ </div>
215
+ <span className="chip chip-info">Active</span>
216
+ </div>
217
+
218
+ <div className="app-item">
219
+ <div className="app-item-info">
220
+ <span className="app-item-name">Email Verification</span>
221
+ <span className="app-item-scope">{user.email}</span>
222
+ </div>
223
+ {user.emailVerified ? (
224
+ <span className="chip chip-success">Verified</span>
225
+ ) : (
226
+ <span className="chip chip-warning">Pending</span>
227
+ )}
228
+ </div>
229
+ </div>
230
+ </div>
231
+
232
+ {/* Sign Out */}
233
+ <div style={{ textAlign: 'center', padding: 'var(--space-4) 0 var(--space-12)' }}>
234
+ <button className="btn btn-danger" onClick={handleSignOut} style={{ padding: '0.75rem 2rem' }}>
235
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
236
+ <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
237
+ <polyline points="16 17 21 12 16 7" />
238
+ <line x1="21" y1="12" x2="9" y2="12" />
239
+ </svg>
240
+ Sign Out
241
+ </button>
242
+ </div>
243
+ </div>
244
+ </>
245
+ );
246
+ }
247
+
248
+ export default function DashboardPage() {
249
+ return (
250
+ <AuthProvider>
251
+ <DashboardContent />
252
+ </AuthProvider>
253
+ );
254
+ }