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.
- package/README.md +222 -0
- package/app/api/internal/create-user/route.js +54 -0
- package/app/api/internal/developer/clients/route.js +122 -0
- package/app/api/internal/generate-code/route.js +41 -0
- package/app/api/oauth/token/route.js +115 -0
- package/app/api/oauth/userinfo/route.js +46 -0
- package/app/consent/page.jsx +202 -0
- package/app/dashboard/page.jsx +254 -0
- package/app/developers/page.jsx +287 -0
- package/app/globals.css +1041 -0
- package/app/layout.jsx +33 -0
- package/app/login/page.jsx +257 -0
- package/app/page.jsx +165 -0
- package/app/register/page.jsx +249 -0
- package/components/DeevoLogo.jsx +41 -0
- package/firebase.json +10 -0
- package/jsconfig.json +7 -0
- package/lib/auth-context.jsx +102 -0
- package/lib/firebase-admin.js +32 -0
- package/lib/firebase.js +18 -0
- package/next.config.mjs +9 -0
- package/package.json +20 -0
- package/public/deevo-logo.svg +3 -0
- package/sdk/README.md +216 -0
- package/sdk/build.js +30 -0
- package/sdk/deevo-oauth-1.4.5.tgz +0 -0
- package/sdk/dist/index.d.ts +69 -0
- package/sdk/dist/index.js +228 -0
- package/sdk/dist/index.mjs +222 -0
- package/sdk/package.json +39 -0
- package/sdk/src/index.d.ts +69 -0
- package/sdk/src/index.js +228 -0
|
@@ -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'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
|
+
}
|