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
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
|
+
}
|