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,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'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
package/jsconfig.json
ADDED
|
@@ -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();
|
package/lib/firebase.js
ADDED
|
@@ -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();
|
package/next.config.mjs
ADDED
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
|
+
}
|