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 ADDED
@@ -0,0 +1,222 @@
1
+ # deevo-oauth
2
+
3
+ Official SDK for integrating **Deevo Account** OAuth 2.0 authentication into your applications. It allows you to add a seamless "Sign in with Deevo" experience, just like Google OAuth!
4
+
5
+ ## Installation
6
+
7
+ Install the package via npm:
8
+
9
+ ```bash
10
+ npm install deevo-oauth
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ### 1. Get Your Credentials
16
+
17
+ You must register your application on the Deevo platform to get your API keys:
18
+ 1. Visit the [Deevo Developer Console](https://deevo.tech/developers).
19
+ 2. Sign in with your Deevo account.
20
+ 3. Click **"+ New App"**.
21
+ 4. Set your application name and the allowed Callback/Redirect URI.
22
+ 5. Save the generated `clientId` and `clientSecret`. Keep the secret secure and never expose it to your frontend!
23
+
24
+ ### 2. Configure the SDK
25
+
26
+ In your backend or server-side code (Node.js, Express, Next.js, etc.), initialize the SDK with your credentials:
27
+
28
+ ```javascript
29
+ const { DeevoAuth } = require('deevo-oauth');
30
+ // or using ES Modules: import { DeevoAuth } from 'deevo-oauth';
31
+
32
+ const deevo = new DeevoAuth({
33
+ clientId: 'YOUR_CLIENT_ID', // e.g. from deevo.tech/developers
34
+ clientSecret: 'YOUR_CLIENT_SECRET', // e.g. from deevo.tech/developers
35
+ redirectUri: 'http://localhost:3000/auth/callback', // The exact URL registered in the console
36
+ });
37
+ ```
38
+
39
+ ### 3. Redirect Users to Sign In
40
+
41
+ When a user clicks "Sign in with Deevo", redirect them to the Deevo authorization URL.
42
+
43
+ **Express.js Example:**
44
+ ```javascript
45
+ app.get('/auth/login', (req, res) => {
46
+ const loginUrl = deevo.getAuthUrl();
47
+ res.redirect(loginUrl);
48
+ });
49
+ ```
50
+
51
+ ### 4. Handle the Callback & Exchange Code
52
+
53
+ After the user successfully signs in, Deevo will redirect them back to your `redirectUri` with a special authorization `code` in the query parameters. You need to exchange this code for the user's profile information.
54
+
55
+ **Express.js Example:**
56
+ ```javascript
57
+ app.get('/auth/callback', async (req, res) => {
58
+ try {
59
+ const code = req.query.code;
60
+
61
+ // This handles both the token exchange and fetching the user profile!
62
+ const { accessToken, user } = await deevo.handleCallback(code);
63
+
64
+ /* user object looks like:
65
+ {
66
+ sub: 'firebase-uid-123',
67
+ name: 'John Doe',
68
+ email: 'john@example.com',
69
+ picture: 'https://...'
70
+ }
71
+ */
72
+
73
+ // Save the user data to your database or session
74
+ req.session.user = user;
75
+ req.session.accessToken = accessToken;
76
+
77
+ // Redirect to your app's dashboard
78
+ res.redirect('/dashboard');
79
+ } catch (error) {
80
+ console.error('Authentication failed:', error);
81
+ res.redirect('/login?error=auth_failed');
82
+ }
83
+ });
84
+ ```
85
+
86
+ ---
87
+
88
+ ## Usage in Next.js (App Router)
89
+
90
+ If you are using Next.js (App Router), here is how you can easily integrate Deevo Auth via API routes:
91
+
92
+ **`app/api/auth/login/route.js`**
93
+ ```javascript
94
+ import { DeevoAuth } from 'deevo-oauth';
95
+ import { redirect } from 'next/navigation';
96
+
97
+ const deevo = new DeevoAuth({
98
+ clientId: process.env.DEEVO_CLIENT_ID,
99
+ clientSecret: process.env.DEEVO_CLIENT_SECRET,
100
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/callback`,
101
+ });
102
+
103
+ export async function GET() {
104
+ // Redirect to Deevo Login Screen
105
+ return Response.redirect(deevo.getAuthUrl());
106
+ }
107
+ ```
108
+
109
+ **`app/api/auth/callback/route.js`**
110
+ ```javascript
111
+ import { DeevoAuth } from 'deevo-oauth';
112
+ import { NextResponse } from 'next/server';
113
+ import { cookies } from 'next/headers';
114
+
115
+ const deevo = new DeevoAuth({
116
+ clientId: process.env.DEEVO_CLIENT_ID,
117
+ clientSecret: process.env.DEEVO_CLIENT_SECRET,
118
+ redirectUri: `${process.env.NEXT_PUBLIC_BASE_URL}/api/auth/callback`,
119
+ });
120
+
121
+ export async function GET(request) {
122
+ const code = request.nextUrl.searchParams.get('code');
123
+
124
+ try {
125
+ const { accessToken, user } = await deevo.handleCallback(code);
126
+
127
+ // Example: Set HTTP-Only Cookie with the token
128
+ const cookieStore = await cookies();
129
+ cookieStore.set('deevo_session', accessToken, {
130
+ httpOnly: true,
131
+ secure: process.env.NODE_ENV === 'production',
132
+ maxAge: 3600 // 1 hour
133
+ });
134
+
135
+ return NextResponse.redirect(new URL('/dashboard', request.url));
136
+ } catch (error) {
137
+ return NextResponse.redirect(new URL('/login?error=invalid_code', request.url));
138
+ }
139
+ }
140
+ ```
141
+
142
+ ---
143
+
144
+ ## API Reference
145
+
146
+ ### `new DeevoAuth(config)`
147
+
148
+ | Parameter | Type | Required | Description |
149
+ |-----------|------|----------|-------------|
150
+ | `clientId` | string | ✅ | Your registered OAuth client ID |
151
+ | `clientSecret` | string | ✅ | Your private client secret (Do not leak this client-side!) |
152
+ | `redirectUri` | string | ✅ | Registered callback URL where users are returned |
153
+ | `authServerUrl` | string | ❌ | Override auth server (Default: `https://deevo.tech`) |
154
+ | `scope` | string | ❌ | Space-separated scopes (Default: `'profile email'`) |
155
+
156
+ ### `deevo.getAuthUrl(options?)`
157
+ Returns the fully-qualified login URL to redirect users to.
158
+ ```javascript
159
+ const url = deevo.getAuthUrl({ state: 'random-csrf-token' });
160
+ ```
161
+
162
+ ### `deevo.exchangeCode(code)`
163
+ Exchanges an authorization code for raw access tokens.
164
+ ```javascript
165
+ const tokens = await deevo.exchangeCode(code);
166
+ // => { access_token: '...', token_type: 'Bearer', expires_in: 3600 }
167
+ ```
168
+
169
+ ### `deevo.getUserInfo(accessToken)`
170
+ Fetches the current user profile data using a valid Bearer token.
171
+ ```javascript
172
+ const user = await deevo.getUserInfo(tokens.access_token);
173
+ ```
174
+
175
+ ### `deevo.handleCallback(code)`
176
+ A convenience method that performs `exchangeCode` AND `getUserInfo` in one swift call.
177
+ ```javascript
178
+ const { accessToken, user } = await deevo.handleCallback(code);
179
+ ```
180
+
181
+ ### `deevo.verifyToken(accessToken)`
182
+ Verifies a token and returns user info. Ideal for protecting your internal API routes.
183
+ ```javascript
184
+ const user = await deevo.verifyToken(req.headers.authorization.split(' ')[1]);
185
+ ```
186
+
187
+ ---
188
+
189
+ ## Express.js API Middleware
190
+ The SDK comes with a built-in Express middleware to protect your private routes:
191
+
192
+ ```javascript
193
+ const { DeevoAuth, deevoMiddleware } = require('deevo-oauth');
194
+
195
+ const deevo = new DeevoAuth({ /* config */ });
196
+
197
+ app.get('/api/protected-data', deevoMiddleware(deevo), (req, res) => {
198
+ // If the token is verified successfully, the user data will be injected into req.deevoUser
199
+ res.json({ message: "Welcome!", user: req.deevoUser });
200
+ });
201
+ ```
202
+
203
+ ## Error Handling
204
+
205
+ If an API call fails, the SDK will throw a `DeevoAuthError`. You can handle specific error codes:
206
+
207
+ ```javascript
208
+ const { DeevoAuthError } = require('deevo-oauth');
209
+
210
+ try {
211
+ const { user } = await deevo.handleCallback(code);
212
+ } catch (error) {
213
+ if (error instanceof DeevoAuthError) {
214
+ console.error(`HTTP Status: ${error.statusCode}`);
215
+ console.error(`Error Code: ${error.code}`);
216
+ console.error(`Message: ${error.message}`);
217
+ }
218
+ }
219
+ ```
220
+
221
+ ---
222
+ **License**: MIT © [Deevo Systems](https://deevo.tech)
@@ -0,0 +1,54 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db, auth } from '@/lib/firebase-admin';
3
+
4
+ export async function POST(request) {
5
+ try {
6
+ const { idToken, fullName } = await request.json();
7
+
8
+ if (!idToken) {
9
+ return NextResponse.json({ error: 'missing_token' }, { status: 400 });
10
+ }
11
+
12
+ // Verify the Firebase ID token
13
+ const decodedToken = await auth.verifyIdToken(idToken);
14
+ const uid = decodedToken.uid;
15
+
16
+ // Get the full user record from Firebase Auth
17
+ const userRecord = await auth.getUser(uid);
18
+
19
+ // Build the user profile data
20
+ const userData = {
21
+ uid: uid,
22
+ email: userRecord.email || '',
23
+ fullName: fullName || userRecord.displayName || '',
24
+ avatarUrl: userRecord.photoURL || '',
25
+ emailVerified: userRecord.emailVerified || false,
26
+ provider: userRecord.providerData?.[0]?.providerId || 'unknown',
27
+ updatedAt: Date.now(),
28
+ };
29
+
30
+ // Check if user already exists
31
+ const userRef = db.collection('users').doc(uid);
32
+ const existingUser = await userRef.get();
33
+
34
+ if (existingUser.exists) {
35
+ // Update existing user (preserve createdAt)
36
+ const updateData = { ...userData };
37
+ delete updateData.uid; // Don't overwrite uid
38
+ await userRef.update(updateData);
39
+ } else {
40
+ // Create new user with createdAt
41
+ userData.createdAt = Date.now();
42
+ await userRef.set(userData);
43
+ }
44
+
45
+ return NextResponse.json({ success: true, uid });
46
+
47
+ } catch (error) {
48
+ console.error('Create User Error:', error);
49
+ return NextResponse.json(
50
+ { error: 'server_error', message: error.message },
51
+ { status: 500 }
52
+ );
53
+ }
54
+ }
@@ -0,0 +1,122 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db, auth } from '@/lib/firebase-admin';
3
+ import crypto from 'crypto';
4
+
5
+ // GET - List all OAuth clients for the authenticated user
6
+ export async function GET(request) {
7
+ try {
8
+ const authHeader = request.headers.get('authorization');
9
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
10
+ return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
11
+ }
12
+
13
+ const idToken = authHeader.split(' ')[1];
14
+ const decodedToken = await auth.verifyIdToken(idToken);
15
+ const uid = decodedToken.uid;
16
+
17
+ // Fetch clients owned by this user
18
+ const snapshot = await db.collection('oauth_clients')
19
+ .where('ownerId', '==', uid)
20
+ .get();
21
+
22
+ const clients = [];
23
+ snapshot.forEach((doc) => {
24
+ const data = doc.data();
25
+ clients.push({
26
+ id: doc.id,
27
+ name: data.name,
28
+ redirectUri: data.redirectUri,
29
+ createdAt: data.createdAt,
30
+ });
31
+ });
32
+
33
+ return NextResponse.json({ clients });
34
+
35
+ } catch (error) {
36
+ console.error('List Clients Error:', error);
37
+ return NextResponse.json({ error: 'server_error' }, { status: 500 });
38
+ }
39
+ }
40
+
41
+ // POST - Create a new OAuth client
42
+ export async function POST(request) {
43
+ try {
44
+ const authHeader = request.headers.get('authorization');
45
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
46
+ return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
47
+ }
48
+
49
+ const idToken = authHeader.split(' ')[1];
50
+ const decodedToken = await auth.verifyIdToken(idToken);
51
+ const uid = decodedToken.uid;
52
+
53
+ const { name, redirectUri } = await request.json();
54
+
55
+ if (!name || !redirectUri) {
56
+ return NextResponse.json({ error: 'missing_fields' }, { status: 400 });
57
+ }
58
+
59
+ // Generate client credentials
60
+ const clientId = crypto.randomBytes(16).toString('hex');
61
+ const clientSecret = `dv_${crypto.randomBytes(32).toString('hex')}`;
62
+
63
+ // Store in Firestore
64
+ await db.collection('oauth_clients').doc(clientId).set({
65
+ name,
66
+ redirectUri,
67
+ clientSecret,
68
+ ownerId: uid,
69
+ createdAt: Date.now(),
70
+ });
71
+
72
+ return NextResponse.json({
73
+ clientId,
74
+ clientSecret,
75
+ name,
76
+ redirectUri,
77
+ });
78
+
79
+ } catch (error) {
80
+ console.error('Create Client Error:', error);
81
+ return NextResponse.json({ error: 'server_error' }, { status: 500 });
82
+ }
83
+ }
84
+
85
+ // DELETE - Delete an OAuth client
86
+ export async function DELETE(request) {
87
+ try {
88
+ const authHeader = request.headers.get('authorization');
89
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
90
+ return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
91
+ }
92
+
93
+ const idToken = authHeader.split(' ')[1];
94
+ const decodedToken = await auth.verifyIdToken(idToken);
95
+ const uid = decodedToken.uid;
96
+
97
+ const { searchParams } = new URL(request.url);
98
+ const clientId = searchParams.get('clientId');
99
+
100
+ if (!clientId) {
101
+ return NextResponse.json({ error: 'missing_client_id' }, { status: 400 });
102
+ }
103
+
104
+ // Verify ownership
105
+ const clientDoc = await db.collection('oauth_clients').doc(clientId).get();
106
+ if (!clientDoc.exists) {
107
+ return NextResponse.json({ error: 'not_found' }, { status: 404 });
108
+ }
109
+
110
+ if (clientDoc.data().ownerId !== uid) {
111
+ return NextResponse.json({ error: 'forbidden' }, { status: 403 });
112
+ }
113
+
114
+ await db.collection('oauth_clients').doc(clientId).delete();
115
+
116
+ return NextResponse.json({ success: true });
117
+
118
+ } catch (error) {
119
+ console.error('Delete Client Error:', error);
120
+ return NextResponse.json({ error: 'server_error' }, { status: 500 });
121
+ }
122
+ }
@@ -0,0 +1,41 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db, auth } from '@/lib/firebase-admin'; // From previous setup
3
+ import crypto from 'crypto';
4
+
5
+ export async function POST(request) {
6
+ try {
7
+ const { idToken, clientId, redirectUri } = await request.json();
8
+
9
+ // 1. Verify the Firebase user securely on the server
10
+ const decodedToken = await auth.verifyIdToken(idToken);
11
+ const uid = decodedToken.uid;
12
+ const email = decodedToken.email;
13
+
14
+ // 2. Validate the Client App
15
+ const clientRef = db.collection('oauth_clients').doc(clientId);
16
+ const clientDoc = await clientRef.get();
17
+
18
+ if (!clientDoc.exists) {
19
+ return NextResponse.json({ error: 'invalid_client' }, { status: 400 });
20
+ }
21
+
22
+ // 3. Generate a secure, random Authorization Code
23
+ const authCode = crypto.randomBytes(16).toString('hex');
24
+
25
+ // 4. Store the code in Firestore with a 5-minute expiration
26
+ await db.collection('oauth_codes').doc(authCode).set({
27
+ code: authCode,
28
+ clientId: clientId,
29
+ uid: uid,
30
+ email: email,
31
+ redirectUri: redirectUri,
32
+ expiresAt: Date.now() + 5 * 60 * 1000, // 5 mins from now
33
+ });
34
+
35
+ return NextResponse.json({ code: authCode });
36
+
37
+ } catch (error) {
38
+ console.error('Error generating auth code:', error);
39
+ return NextResponse.json({ error: 'server_error' }, { status: 500 });
40
+ }
41
+ }
@@ -0,0 +1,115 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db, auth } from '@/lib/firebase-admin';
3
+ import jwt from 'jsonwebtoken';
4
+
5
+ export async function POST(request) {
6
+ try {
7
+ const body = await request.json();
8
+ const { grant_type, code, client_id, client_secret, redirect_uri } = body;
9
+
10
+ // Validate grant type
11
+ if (grant_type !== 'authorization_code') {
12
+ return NextResponse.json(
13
+ { error: 'unsupported_grant_type', message: 'Only authorization_code is supported' },
14
+ { status: 400 }
15
+ );
16
+ }
17
+
18
+ // Validate required fields
19
+ if (!code || !client_id || !redirect_uri) {
20
+ return NextResponse.json(
21
+ { error: 'invalid_request', message: 'Missing required fields: code, client_id, redirect_uri' },
22
+ { status: 400 }
23
+ );
24
+ }
25
+
26
+ // 1. Look up the auth code in Firestore
27
+ const codeRef = db.collection('oauth_codes').doc(code);
28
+ const codeDoc = await codeRef.get();
29
+
30
+ if (!codeDoc.exists) {
31
+ return NextResponse.json(
32
+ { error: 'invalid_grant', message: 'Authorization code not found or already used' },
33
+ { status: 400 }
34
+ );
35
+ }
36
+
37
+ const codeData = codeDoc.data();
38
+
39
+ // 2. Validate the code hasn't expired
40
+ if (Date.now() > codeData.expiresAt) {
41
+ await codeRef.delete(); // Clean up expired code
42
+ return NextResponse.json(
43
+ { error: 'invalid_grant', message: 'Authorization code has expired' },
44
+ { status: 400 }
45
+ );
46
+ }
47
+
48
+ // 3. Validate the client_id and redirect_uri match
49
+ if (codeData.clientId !== client_id) {
50
+ return NextResponse.json(
51
+ { error: 'invalid_client', message: 'Client ID mismatch' },
52
+ { status: 400 }
53
+ );
54
+ }
55
+
56
+ if (codeData.redirectUri !== redirect_uri) {
57
+ return NextResponse.json(
58
+ { error: 'invalid_grant', message: 'Redirect URI mismatch' },
59
+ { status: 400 }
60
+ );
61
+ }
62
+
63
+ // 4. Validate client_secret (if provided)
64
+ if (client_secret) {
65
+ const clientRef = db.collection('oauth_clients').doc(client_id);
66
+ const clientDoc = await clientRef.get();
67
+
68
+ if (!clientDoc.exists) {
69
+ return NextResponse.json(
70
+ { error: 'invalid_client', message: 'Client not found' },
71
+ { status: 400 }
72
+ );
73
+ }
74
+
75
+ const clientData = clientDoc.data();
76
+ if (clientData.clientSecret && clientData.clientSecret !== client_secret) {
77
+ return NextResponse.json(
78
+ { error: 'invalid_client', message: 'Invalid client secret' },
79
+ { status: 401 }
80
+ );
81
+ }
82
+ }
83
+
84
+ // 5. Delete the code (one-time use)
85
+ await codeRef.delete();
86
+
87
+ // 6. Generate a Deevo access token (JWT)
88
+ const accessToken = jwt.sign(
89
+ {
90
+ uid: codeData.uid,
91
+ email: codeData.email,
92
+ client_id: client_id,
93
+ iss: 'https://deevo.tech',
94
+ aud: client_id,
95
+ },
96
+ process.env.DEEVO_JWT_SECRET,
97
+ { expiresIn: '1h' }
98
+ );
99
+
100
+ // 7. Return the token response (OAuth 2.0 standard format)
101
+ return NextResponse.json({
102
+ access_token: accessToken,
103
+ token_type: 'Bearer',
104
+ expires_in: 3600,
105
+ scope: 'profile email',
106
+ });
107
+
108
+ } catch (error) {
109
+ console.error('Token Exchange Error:', error);
110
+ return NextResponse.json(
111
+ { error: 'server_error', message: 'Internal server error' },
112
+ { status: 500 }
113
+ );
114
+ }
115
+ }
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { db } from '@/lib/firebase-admin';
3
+ import jwt from 'jsonwebtoken';
4
+
5
+ export async function GET(request) {
6
+ try {
7
+ // Extract the Bearer token from the header
8
+ const authHeader = request.headers.get('authorization');
9
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
10
+ return NextResponse.json({ error: 'invalid_token' }, { status: 401 });
11
+ }
12
+
13
+ const token = authHeader.split(' ')[1];
14
+
15
+ // Verify the JWT was signed by Deevo
16
+ let decodedPayload;
17
+ try {
18
+ decodedPayload = jwt.verify(token, process.env.DEEVO_JWT_SECRET);
19
+ } catch (err) {
20
+ return NextResponse.json({ error: 'invalid_token', message: 'Token expired or malformed' }, { status: 401 });
21
+ }
22
+
23
+ // Fetch the canonical user profile from Firestore using the UID in the token
24
+ const userRef = db.collection('users').doc(decodedPayload.uid);
25
+ const userDoc = await userRef.get();
26
+
27
+ if (!userDoc.exists) {
28
+ return NextResponse.json({ error: 'user_not_found' }, { status: 404 });
29
+ }
30
+
31
+ const userData = userDoc.data();
32
+
33
+ // Return standard OpenID Connect / OAuth userinfo response
34
+ return NextResponse.json({
35
+ sub: decodedPayload.uid,
36
+ name: userData.fullName || '',
37
+ email: userData.email,
38
+ picture: userData.avatarUrl || '',
39
+ // Add any custom Deevo ecosystem claims here
40
+ });
41
+
42
+ } catch (error) {
43
+ console.error('UserInfo Error:', error);
44
+ return NextResponse.json({ error: 'server_error' }, { status: 500 });
45
+ }
46
+ }