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,249 @@
1
+ 'use client';
2
+
3
+ import { useState, Suspense, useEffect } from 'react';
4
+ import { useSearchParams, useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+ import { AuthProvider, useAuth } from '@/lib/auth-context';
7
+
8
+ function RegisterContent() {
9
+ const searchParams = useSearchParams();
10
+ const router = useRouter();
11
+ const { user, loading: authLoading, signUpWithEmail, signInWithGoogle } = useAuth();
12
+
13
+ const [fullName, setFullName] = useState('');
14
+ const [email, setEmail] = useState('');
15
+ const [password, setPassword] = useState('');
16
+ const [confirmPassword, setConfirmPassword] = useState('');
17
+ const [loading, setLoading] = useState(false);
18
+ const [googleLoading, setGoogleLoading] = useState(false);
19
+ const [error, setError] = useState('');
20
+ const [success, setSuccess] = useState(false);
21
+ const [initChecking, setInitChecking] = useState(true);
22
+
23
+ const clientId = searchParams.get('client_id');
24
+ const redirectUri = searchParams.get('redirect_uri');
25
+ const isOAuthFlow = !!(clientId && redirectUri);
26
+
27
+ useEffect(() => {
28
+ if (!authLoading) {
29
+ if (user) {
30
+ handleOAuthRedirect(user);
31
+ } else {
32
+ setInitChecking(false);
33
+ }
34
+ }
35
+ }, [user, authLoading]);
36
+
37
+ const getPasswordStrength = (pwd) => {
38
+ if (!pwd) return 0;
39
+ let score = 0;
40
+ if (pwd.length >= 6) score++;
41
+ if (pwd.length >= 10) score++;
42
+ if (/[A-Z]/.test(pwd) && /[a-z]/.test(pwd)) score++;
43
+ if (/\d/.test(pwd)) score++;
44
+ if (/[^A-Za-z0-9]/.test(pwd)) score++;
45
+ return Math.min(score, 4);
46
+ };
47
+
48
+ const strength = getPasswordStrength(password);
49
+
50
+ const handleOAuthRedirect = async (user) => {
51
+ if (!isOAuthFlow) {
52
+ router.push('/dashboard');
53
+ return;
54
+ }
55
+ const idToken = await user.getIdToken();
56
+ const response = await fetch('/api/internal/generate-code', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({ idToken, clientId, redirectUri }),
60
+ });
61
+ const data = await response.json();
62
+ if (data.error) throw new Error(data.error);
63
+ window.location.href = `${redirectUri}?code=${data.code}`;
64
+ };
65
+
66
+ const handleGoogleSignup = async () => {
67
+ setError('');
68
+ setGoogleLoading(true);
69
+ try {
70
+ const user = await signInWithGoogle();
71
+ const idToken = await user.getIdToken();
72
+ await fetch('/api/internal/create-user', {
73
+ method: 'POST',
74
+ headers: { 'Content-Type': 'application/json' },
75
+ body: JSON.stringify({ idToken, fullName: user.displayName || '' }),
76
+ });
77
+ await handleOAuthRedirect(user);
78
+ } catch (err) {
79
+ console.error(err);
80
+ if (err.code !== 'auth/popup-closed-by-user') {
81
+ setError('Google sign up failed. Please try again.');
82
+ }
83
+ setGoogleLoading(false);
84
+ }
85
+ };
86
+
87
+ const handleEmailSignup = async (e) => {
88
+ e.preventDefault();
89
+ if (!fullName.trim()) { setError('Please enter your full name.'); return; }
90
+ if (!email || !password) { setError('Please fill in all fields.'); return; }
91
+ if (password.length < 6) { setError('Password must be at least 6 characters.'); return; }
92
+ if (password !== confirmPassword) { setError('Passwords do not match.'); return; }
93
+
94
+ setError('');
95
+ setLoading(true);
96
+ try {
97
+ const user = await signUpWithEmail(email, password, fullName);
98
+ setSuccess(true);
99
+ setTimeout(async () => { await handleOAuthRedirect(user); }, 2000);
100
+ } catch (err) {
101
+ console.error(err);
102
+ if (err.code === 'auth/email-already-in-use') {
103
+ setError('An account with this email already exists.');
104
+ } else if (err.code === 'auth/weak-password') {
105
+ setError('Password is too weak. Use at least 6 characters.');
106
+ } else if (err.code === 'auth/invalid-email') {
107
+ setError('Please enter a valid email address.');
108
+ } else {
109
+ setError('Registration failed. Please try again.');
110
+ }
111
+ setLoading(false);
112
+ }
113
+ };
114
+
115
+ const loginHref = isOAuthFlow
116
+ ? `/login?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}`
117
+ : '/login';
118
+
119
+ if (initChecking || authLoading || success) {
120
+ if (success) {
121
+ return (
122
+ <>
123
+ <div className="bg-animated" />
124
+ <div className="page-center">
125
+ <div className="auth-container">
126
+ <div className="glass-card auth-card" style={{ textAlign: 'center' }}>
127
+ <div style={{ fontSize: '2rem', margin: '0 auto var(--space-2)', textAlign: 'center', color: 'var(--success)' }}>✓</div>
128
+ <img src="/deevo-logo.svg" alt="Deevo" style={{ height: 28, width: 'auto', margin: '0 auto var(--space-6)', display: 'block' }} />
129
+ <h1 style={{ fontSize: 'var(--font-size-2xl)', fontWeight: 600, marginBottom: 'var(--space-3)' }}>
130
+ Account Created!
131
+ </h1>
132
+ <p style={{ color: 'var(--on-surface-variant)', fontSize: 'var(--font-size-sm)', marginBottom: 'var(--space-4)' }}>
133
+ We&apos;ve sent a verification email to <strong style={{ color: 'var(--primary)' }}>{email}</strong>.
134
+ </p>
135
+ <div className="alert alert-info"><span>Redirecting you shortly...</span></div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </>
140
+ );
141
+ }
142
+
143
+ // Default loading state
144
+ return (
145
+ <div className="loading-overlay">
146
+ <img src="/deevo-logo.svg" alt="Deevo" style={{ height: 32, width: 'auto' }} />
147
+ <span className="spinner" style={{ width: 24, height: 24, color: 'var(--primary)', marginTop: 'var(--space-4)' }} />
148
+ </div>
149
+ );
150
+ }
151
+
152
+ return (
153
+ <>
154
+ <div className="bg-animated" />
155
+ <div className="page-center">
156
+ <div className="auth-container">
157
+ <div className="glass-card auth-card">
158
+ <img src="/deevo-logo.svg" alt="Deevo" style={{ height: 28, width: 'auto', margin: '0 auto var(--space-6)', display: 'block' }} />
159
+
160
+ <div className="auth-header">
161
+ <h1>Create your Deevo Account</h1>
162
+ <p>One account for the entire Deevo ecosystem</p>
163
+ </div>
164
+
165
+ {error && (
166
+ <div className="alert alert-error" style={{ marginBottom: 'var(--space-5)' }}>
167
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ flexShrink: 0, marginTop: '2px' }}>
168
+ <circle cx="12" cy="12" r="10" /><line x1="15" y1="9" x2="9" y2="15" /><line x1="9" y1="9" x2="15" y2="15" />
169
+ </svg>
170
+ <span>{error}</span>
171
+ </div>
172
+ )}
173
+
174
+ <form className="auth-form" onSubmit={handleEmailSignup}>
175
+ <div className="form-group">
176
+ <label className="form-label" htmlFor="reg-name">Full Name</label>
177
+ <input id="reg-name" className="form-input" type="text" placeholder="John Doe" value={fullName} onChange={(e) => setFullName(e.target.value)} autoComplete="name" />
178
+ </div>
179
+ <div className="form-group">
180
+ <label className="form-label" htmlFor="reg-email">Email</label>
181
+ <input id="reg-email" className="form-input" type="email" placeholder="you@example.com" value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="email" />
182
+ </div>
183
+ <div className="form-group">
184
+ <label className="form-label" htmlFor="reg-password">Password</label>
185
+ <input id="reg-password" className="form-input" type="password" placeholder="At least 6 characters" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="new-password" />
186
+ {password && (
187
+ <div className="password-strength">
188
+ {[1, 2, 3, 4].map((level) => (
189
+ <div key={level} className={`password-strength-bar ${strength >= level ? (strength <= 1 ? 'active-weak' : strength <= 2 ? 'active-medium' : 'active-strong') : ''}`} />
190
+ ))}
191
+ </div>
192
+ )}
193
+ </div>
194
+ <div className="form-group">
195
+ <label className="form-label" htmlFor="reg-confirm">Confirm Password</label>
196
+ <input id="reg-confirm" className="form-input" type="password" placeholder="Re-enter your password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} autoComplete="new-password" />
197
+ </div>
198
+ <button type="submit" className="btn btn-primary btn-full" disabled={loading || googleLoading}>
199
+ {loading ? (<><span className="spinner" /> Creating account...</>) : 'Create Account'}
200
+ </button>
201
+ </form>
202
+
203
+ <div className="divider" style={{ margin: 'var(--space-6) 0' }}><span>or</span></div>
204
+
205
+ <button className="btn btn-social" onClick={handleGoogleSignup} disabled={loading || googleLoading}>
206
+ {googleLoading ? (<><span className="spinner" /> Connecting...</>) : (
207
+ <>
208
+ <svg width="20" height="20" viewBox="0 0 24 24">
209
+ <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" />
210
+ <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" />
211
+ <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" />
212
+ <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" />
213
+ </svg>
214
+ Continue with Google
215
+ </>
216
+ )}
217
+ </button>
218
+
219
+ <div className="auth-footer">
220
+ Already have an account?{' '}<Link href={loginHref}>Sign in</Link>
221
+ </div>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ </>
226
+ );
227
+ }
228
+
229
+ function RegisterFallback() {
230
+ return (
231
+ <div className="page-center">
232
+ <div className="auth-container">
233
+ <div className="glass-card auth-card" style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-16)' }}>
234
+ <span className="spinner" style={{ width: 32, height: 32 }} />
235
+ </div>
236
+ </div>
237
+ </div>
238
+ );
239
+ }
240
+
241
+ export default function RegisterPage() {
242
+ return (
243
+ <AuthProvider>
244
+ <Suspense fallback={<RegisterFallback />}>
245
+ <RegisterContent />
246
+ </Suspense>
247
+ </AuthProvider>
248
+ );
249
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import Link from 'next/link';
4
+
5
+ export default function DeevoLogo({ size = 'default', linkTo = '/', showIcon = true }) {
6
+ const sizes = {
7
+ small: { icon: 28, text: 80, fontSize: '1rem' },
8
+ default: { icon: 36, text: 100, fontSize: '1.125rem' },
9
+ large: { icon: 48, text: 140, fontSize: '1.5rem' },
10
+ xlarge: { icon: 56, text: 160, fontSize: '1.75rem' },
11
+ };
12
+
13
+ const s = sizes[size] || sizes.default;
14
+
15
+ return (
16
+ <Link href={linkTo} className="navbar-brand" style={{ textDecoration: 'none' }}>
17
+ {showIcon && (
18
+ <div
19
+ className="navbar-logo"
20
+ style={{
21
+ width: s.icon,
22
+ height: s.icon,
23
+ fontSize: `${s.icon * 0.45}px`,
24
+ borderRadius: `${s.icon * 0.25}px`,
25
+ }}
26
+ >
27
+ D
28
+ </div>
29
+ )}
30
+ <img
31
+ src="/deevo-logo.svg"
32
+ alt="Deevo"
33
+ style={{
34
+ height: `${s.icon * 0.55}px`,
35
+ width: 'auto',
36
+ filter: 'brightness(1)',
37
+ }}
38
+ />
39
+ </Link>
40
+ );
41
+ }
package/firebase.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "hosting": {
3
+ "public": "public",
4
+ "ignore": [
5
+ "firebase.json",
6
+ "**/.*",
7
+ "**/node_modules/**"
8
+ ]
9
+ }
10
+ }
package/jsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "@/*": ["./*"]
5
+ }
6
+ }
7
+ }
@@ -0,0 +1,102 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useEffect, useState } from 'react';
4
+ import {
5
+ onAuthStateChanged,
6
+ signInWithPopup,
7
+ signInWithEmailAndPassword,
8
+ createUserWithEmailAndPassword,
9
+ signOut as firebaseSignOut,
10
+ sendEmailVerification,
11
+ updateProfile,
12
+ } from 'firebase/auth';
13
+ import { auth, googleProvider } from '@/lib/firebase';
14
+
15
+ const AuthContext = createContext({});
16
+
17
+ export function AuthProvider({ children }) {
18
+ const [user, setUser] = useState(null);
19
+ const [loading, setLoading] = useState(true);
20
+
21
+ useEffect(() => {
22
+ const unsubscribe = onAuthStateChanged(auth, (firebaseUser) => {
23
+ setUser(firebaseUser);
24
+ setLoading(false);
25
+ });
26
+
27
+ return () => unsubscribe();
28
+ }, []);
29
+
30
+ // Sign in with Google
31
+ const signInWithGoogle = async () => {
32
+ const result = await signInWithPopup(auth, googleProvider);
33
+ return result.user;
34
+ };
35
+
36
+ // Sign in with email/password
37
+ const signInWithEmail = async (email, password) => {
38
+ const result = await signInWithEmailAndPassword(auth, email, password);
39
+ return result.user;
40
+ };
41
+
42
+ // Create account with email/password
43
+ const signUpWithEmail = async (email, password, displayName) => {
44
+ const result = await createUserWithEmailAndPassword(auth, email, password);
45
+
46
+ // Set user display name
47
+ if (displayName) {
48
+ await updateProfile(result.user, { displayName });
49
+ }
50
+
51
+ // Send email verification via Firebase
52
+ await sendEmailVerification(result.user);
53
+
54
+ // Create user profile in Firestore via API
55
+ const idToken = await result.user.getIdToken();
56
+ await fetch('/api/internal/create-user', {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ body: JSON.stringify({
60
+ idToken,
61
+ fullName: displayName || '',
62
+ }),
63
+ });
64
+
65
+ return result.user;
66
+ };
67
+
68
+ // Sign out
69
+ const signOut = async () => {
70
+ await firebaseSignOut(auth);
71
+ };
72
+
73
+ // Get Firebase ID Token (for API calls)
74
+ const getIdToken = async () => {
75
+ if (!user) return null;
76
+ return await user.getIdToken();
77
+ };
78
+
79
+ const value = {
80
+ user,
81
+ loading,
82
+ signInWithGoogle,
83
+ signInWithEmail,
84
+ signUpWithEmail,
85
+ signOut,
86
+ getIdToken,
87
+ };
88
+
89
+ return (
90
+ <AuthContext.Provider value={value}>
91
+ {children}
92
+ </AuthContext.Provider>
93
+ );
94
+ }
95
+
96
+ export function useAuth() {
97
+ const context = useContext(AuthContext);
98
+ if (!context) {
99
+ throw new Error('useAuth must be used within an AuthProvider');
100
+ }
101
+ return context;
102
+ }
@@ -0,0 +1,32 @@
1
+ import admin from 'firebase-admin';
2
+
3
+ if (!admin.apps.length) {
4
+ try {
5
+ let saString = process.env.FIREBASE_SERVICE_ACCOUNT_KEY;
6
+
7
+ if (!saString) {
8
+ console.error('CRITICAL: FIREBASE_SERVICE_ACCOUNT_KEY is missing from environment variables!');
9
+ } else {
10
+ // If user pasted the single quotes from .env into Vercel, strip them before parsing
11
+ if (saString.startsWith("'") && saString.endsWith("'")) {
12
+ saString = saString.slice(1, -1);
13
+ }
14
+
15
+ const serviceAccount = JSON.parse(saString);
16
+
17
+ // Fix stringified newlines from environment variables
18
+ if (serviceAccount.private_key) {
19
+ serviceAccount.private_key = serviceAccount.private_key.replace(/\\n/g, '\n');
20
+ }
21
+
22
+ admin.initializeApp({
23
+ credential: admin.credential.cert(serviceAccount),
24
+ });
25
+ }
26
+ } catch (err) {
27
+ console.error('Firebase Admin Init Error! Check if your FIREBASE_SERVICE_ACCOUNT_KEY json is valid:', err);
28
+ }
29
+ }
30
+
31
+ export const db = admin.firestore();
32
+ export const auth = admin.auth();
@@ -0,0 +1,18 @@
1
+ import { initializeApp, getApps } from 'firebase/app';
2
+ import { getAuth, GoogleAuthProvider } from 'firebase/auth';
3
+
4
+ const firebaseConfig = {
5
+ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
6
+ authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
7
+ projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
8
+ storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
9
+ messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
10
+ appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
11
+ measurementId: process.env.NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID,
12
+ };
13
+
14
+ // Prevent re-initialization in Next.js development mode
15
+ const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
16
+
17
+ export const auth = getAuth(app);
18
+ export const googleProvider = new GoogleAuthProvider();
@@ -0,0 +1,9 @@
1
+ /** @type {import('next').NextConfig} */
2
+ const nextConfig = {
3
+ reactStrictMode: true,
4
+ images: {
5
+ domains: ['lh3.googleusercontent.com', 'firebasestorage.googleapis.com'],
6
+ },
7
+ };
8
+
9
+ export default nextConfig;
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "deevoauth",
3
+ "version": "1.4.5",
4
+ "private": false,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start"
9
+ },
10
+ "dependencies": {
11
+ "deevoauth": "^1.4.5",
12
+ "firebase": "^11.6.0",
13
+ "firebase-admin": "^13.8.0",
14
+ "jsonwebtoken": "^9.0.3",
15
+ "next": "^15.3.1",
16
+ "react": "^19.1.0",
17
+ "react-dom": "^19.1.0",
18
+ "resend": "^4.2.0"
19
+ }
20
+ }
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 32" fill="none">
2
+ <text x="0" y="26" font-family="Inter, -apple-system, sans-serif" font-size="28" font-weight="300" fill="white" letter-spacing="-0.5">deevo</text>
3
+ </svg>