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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deevo Auth SDK
|
|
3
|
+
* Official SDK for integrating Deevo Account OAuth 2.0 into your applications.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { DeevoAuth } from 'deevo-auth';
|
|
7
|
+
*
|
|
8
|
+
* const deevo = new DeevoAuth({
|
|
9
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
10
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
11
|
+
* redirectUri: 'https://yourapp.com/auth/callback',
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEFAULT_AUTH_URL = 'https://deevo.tech';
|
|
16
|
+
|
|
17
|
+
class DeevoAuth {
|
|
18
|
+
/**
|
|
19
|
+
* Create a new DeevoAuth instance.
|
|
20
|
+
* @param {Object} config
|
|
21
|
+
* @param {string} config.clientId - Your OAuth client ID from the Deevo Developer Console
|
|
22
|
+
* @param {string} config.clientSecret - Your OAuth client secret (keep this server-side only!)
|
|
23
|
+
* @param {string} config.redirectUri - The callback URL registered in your Deevo app
|
|
24
|
+
* @param {string} [config.authServerUrl] - Override the auth server URL (default: https://deevo.tech)
|
|
25
|
+
* @param {string} [config.scope] - Space-separated scopes (default: 'profile email')
|
|
26
|
+
*/
|
|
27
|
+
constructor(config) {
|
|
28
|
+
if (!config.clientId) throw new Error('DeevoAuth: clientId is required');
|
|
29
|
+
if (!config.clientSecret) throw new Error('DeevoAuth: clientSecret is required');
|
|
30
|
+
if (!config.redirectUri) throw new Error('DeevoAuth: redirectUri is required');
|
|
31
|
+
|
|
32
|
+
this.clientId = config.clientId;
|
|
33
|
+
this.clientSecret = config.clientSecret;
|
|
34
|
+
this.redirectUri = config.redirectUri;
|
|
35
|
+
this.authServerUrl = (config.authServerUrl || DEFAULT_AUTH_URL).replace(/\/$/, '');
|
|
36
|
+
this.scope = config.scope || 'profile email';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the URL to redirect users to for authentication.
|
|
41
|
+
* Redirect the user's browser to this URL to start the OAuth flow.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} [options]
|
|
44
|
+
* @param {string} [options.state] - Optional state parameter for CSRF protection
|
|
45
|
+
* @returns {string} The authorization URL
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Express.js route
|
|
49
|
+
* app.get('/auth/login', (req, res) => {
|
|
50
|
+
* const url = deevo.getAuthUrl({ state: 'random-csrf-token' });
|
|
51
|
+
* res.redirect(url);
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
getAuthUrl(options = {}) {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
client_id: this.clientId,
|
|
57
|
+
redirect_uri: this.redirectUri,
|
|
58
|
+
scope: this.scope,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (options.state) {
|
|
62
|
+
params.set('state', options.state);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `${this.authServerUrl}/login?${params.toString()}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Exchange an authorization code for an access token.
|
|
70
|
+
* Call this in your callback route after the user is redirected back.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} code - The authorization code from the callback URL query params
|
|
73
|
+
* @returns {Promise<Object>} Token response with access_token, token_type, expires_in
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Express.js callback route
|
|
77
|
+
* app.get('/auth/callback', async (req, res) => {
|
|
78
|
+
* const { code } = req.query;
|
|
79
|
+
* const tokens = await deevo.exchangeCode(code);
|
|
80
|
+
* // tokens.access_token is your bearer token
|
|
81
|
+
* });
|
|
82
|
+
*/
|
|
83
|
+
async exchangeCode(code) {
|
|
84
|
+
const response = await fetch(`${this.authServerUrl}/api/oauth/token`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
grant_type: 'authorization_code',
|
|
89
|
+
code,
|
|
90
|
+
client_id: this.clientId,
|
|
91
|
+
client_secret: this.clientSecret,
|
|
92
|
+
redirect_uri: this.redirectUri,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await response.json().catch(() => ({}));
|
|
98
|
+
throw new DeevoAuthError(
|
|
99
|
+
error.message || `Token exchange failed with status ${response.status}`,
|
|
100
|
+
error.error || 'token_exchange_failed',
|
|
101
|
+
response.status
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return response.json();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the authenticated user's profile information.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} accessToken - The access token from exchangeCode()
|
|
112
|
+
* @returns {Promise<Object>} User profile { sub, name, email, picture }
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* const user = await deevo.getUserInfo(tokens.access_token);
|
|
116
|
+
* console.log(user.email, user.name);
|
|
117
|
+
*/
|
|
118
|
+
async getUserInfo(accessToken) {
|
|
119
|
+
const response = await fetch(`${this.authServerUrl}/api/oauth/userinfo`, {
|
|
120
|
+
headers: {
|
|
121
|
+
Authorization: `Bearer ${accessToken}`,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const error = await response.json().catch(() => ({}));
|
|
127
|
+
throw new DeevoAuthError(
|
|
128
|
+
error.message || `UserInfo request failed with status ${response.status}`,
|
|
129
|
+
error.error || 'userinfo_failed',
|
|
130
|
+
response.status
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return response.json();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Complete OAuth flow in one call: exchange code and get user info.
|
|
139
|
+
* Convenience method combining exchangeCode() + getUserInfo().
|
|
140
|
+
*
|
|
141
|
+
* @param {string} code - The authorization code from the callback URL
|
|
142
|
+
* @returns {Promise<Object>} { accessToken, tokenType, expiresIn, user }
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* app.get('/auth/callback', async (req, res) => {
|
|
146
|
+
* const { accessToken, user } = await deevo.handleCallback(req.query.code);
|
|
147
|
+
* req.session.user = user;
|
|
148
|
+
* req.session.accessToken = accessToken;
|
|
149
|
+
* res.redirect('/dashboard');
|
|
150
|
+
* });
|
|
151
|
+
*/
|
|
152
|
+
async handleCallback(code) {
|
|
153
|
+
const tokens = await this.exchangeCode(code);
|
|
154
|
+
const user = await this.getUserInfo(tokens.access_token);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
accessToken: tokens.access_token,
|
|
158
|
+
tokenType: tokens.token_type,
|
|
159
|
+
expiresIn: tokens.expires_in,
|
|
160
|
+
user,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Verify an access token and get user info (useful for API middleware).
|
|
166
|
+
*
|
|
167
|
+
* @param {string} accessToken - The Bearer token to verify
|
|
168
|
+
* @returns {Promise<Object>} User profile if valid
|
|
169
|
+
* @throws {DeevoAuthError} If the token is invalid or expired
|
|
170
|
+
*/
|
|
171
|
+
async verifyToken(accessToken) {
|
|
172
|
+
return this.getUserInfo(accessToken);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Express.js middleware for protecting routes with Deevo Auth.
|
|
178
|
+
*
|
|
179
|
+
* @param {DeevoAuth} deevoAuth - A configured DeevoAuth instance
|
|
180
|
+
* @returns {Function} Express middleware
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* app.get('/api/protected', deevoMiddleware(deevo), (req, res) => {
|
|
184
|
+
* res.json({ user: req.deevoUser });
|
|
185
|
+
* });
|
|
186
|
+
*/
|
|
187
|
+
function deevoMiddleware(deevoAuth) {
|
|
188
|
+
return async (req, res, next) => {
|
|
189
|
+
const authHeader = req.headers.authorization;
|
|
190
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
191
|
+
return res.status(401).json({ error: 'Missing or invalid authorization header' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const token = authHeader.split(' ')[1];
|
|
195
|
+
try {
|
|
196
|
+
const user = await deevoAuth.verifyToken(token);
|
|
197
|
+
req.deevoUser = user;
|
|
198
|
+
next();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Custom error class for Deevo Auth errors.
|
|
207
|
+
*/
|
|
208
|
+
class DeevoAuthError extends Error {
|
|
209
|
+
constructor(message, code, statusCode) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = 'DeevoAuthError';
|
|
212
|
+
this.code = code;
|
|
213
|
+
this.statusCode = statusCode;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Export for CommonJS
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
// Export for ESM
|
|
221
|
+
export { DeevoAuth, deevoMiddleware, DeevoAuthError };
|
|
222
|
+
export default DeevoAuth;
|
package/sdk/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "deevoauth",
|
|
3
|
+
"version": "1.4.6",
|
|
4
|
+
"description": "Official SDK for Deevo Account OAuth 2.0 authentication. Add 'Sign in with Deevo' to your app.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "node build.js",
|
|
21
|
+
"prepublishOnly": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"deevo",
|
|
25
|
+
"auth",
|
|
26
|
+
"oauth",
|
|
27
|
+
"oauth2",
|
|
28
|
+
"authentication",
|
|
29
|
+
"login",
|
|
30
|
+
"identity"
|
|
31
|
+
],
|
|
32
|
+
"author": "Deevo",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"homepage": "https://deevo.tech",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/deevo-tech/deevo-auth"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export interface DeevoAuthConfig {
|
|
2
|
+
/** Your OAuth client ID from the Deevo Developer Console */
|
|
3
|
+
clientId: string;
|
|
4
|
+
/** Your OAuth client secret (keep server-side only!) */
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
/** The callback URL registered in your Deevo app */
|
|
7
|
+
redirectUri: string;
|
|
8
|
+
/** Override the auth server URL (default: https://deevo.tech) */
|
|
9
|
+
authServerUrl?: string;
|
|
10
|
+
/** Space-separated scopes (default: 'profile email') */
|
|
11
|
+
scope?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TokenResponse {
|
|
15
|
+
access_token: string;
|
|
16
|
+
token_type: string;
|
|
17
|
+
expires_in: number;
|
|
18
|
+
scope?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface DeevoUser {
|
|
22
|
+
/** Unique user identifier */
|
|
23
|
+
sub: string;
|
|
24
|
+
/** User's full name */
|
|
25
|
+
name: string;
|
|
26
|
+
/** User's email address */
|
|
27
|
+
email: string;
|
|
28
|
+
/** URL to user's profile picture */
|
|
29
|
+
picture: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CallbackResult {
|
|
33
|
+
accessToken: string;
|
|
34
|
+
tokenType: string;
|
|
35
|
+
expiresIn: number;
|
|
36
|
+
user: DeevoUser;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class DeevoAuth {
|
|
40
|
+
constructor(config: DeevoAuthConfig);
|
|
41
|
+
|
|
42
|
+
/** Get the URL to redirect users to for authentication */
|
|
43
|
+
getAuthUrl(options?: { state?: string }): string;
|
|
44
|
+
|
|
45
|
+
/** Exchange an authorization code for tokens */
|
|
46
|
+
exchangeCode(code: string): Promise<TokenResponse>;
|
|
47
|
+
|
|
48
|
+
/** Get the authenticated user's profile information */
|
|
49
|
+
getUserInfo(accessToken: string): Promise<DeevoUser>;
|
|
50
|
+
|
|
51
|
+
/** Complete OAuth flow: exchange code + get user info */
|
|
52
|
+
handleCallback(code: string): Promise<CallbackResult>;
|
|
53
|
+
|
|
54
|
+
/** Verify an access token and get user info */
|
|
55
|
+
verifyToken(accessToken: string): Promise<DeevoUser>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class DeevoAuthError extends Error {
|
|
59
|
+
code: string;
|
|
60
|
+
statusCode: number;
|
|
61
|
+
constructor(message: string, code: string, statusCode: number);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Express.js middleware for protecting routes */
|
|
65
|
+
export function deevoMiddleware(
|
|
66
|
+
deevoAuth: DeevoAuth
|
|
67
|
+
): (req: any, res: any, next: any) => Promise<void>;
|
|
68
|
+
|
|
69
|
+
export default DeevoAuth;
|
package/sdk/src/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deevo Auth SDK
|
|
3
|
+
* Official SDK for integrating Deevo Account OAuth 2.0 into your applications.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* import { DeevoAuth } from 'deevo-auth';
|
|
7
|
+
*
|
|
8
|
+
* const deevo = new DeevoAuth({
|
|
9
|
+
* clientId: 'YOUR_CLIENT_ID',
|
|
10
|
+
* clientSecret: 'YOUR_CLIENT_SECRET',
|
|
11
|
+
* redirectUri: 'https://yourapp.com/auth/callback',
|
|
12
|
+
* });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const DEFAULT_AUTH_URL = 'https://deevo.tech';
|
|
16
|
+
|
|
17
|
+
class DeevoAuth {
|
|
18
|
+
/**
|
|
19
|
+
* Create a new DeevoAuth instance.
|
|
20
|
+
* @param {Object} config
|
|
21
|
+
* @param {string} config.clientId - Your OAuth client ID from the Deevo Developer Console
|
|
22
|
+
* @param {string} config.clientSecret - Your OAuth client secret (keep this server-side only!)
|
|
23
|
+
* @param {string} config.redirectUri - The callback URL registered in your Deevo app
|
|
24
|
+
* @param {string} [config.authServerUrl] - Override the auth server URL (default: https://deevo.tech)
|
|
25
|
+
* @param {string} [config.scope] - Space-separated scopes (default: 'profile email')
|
|
26
|
+
*/
|
|
27
|
+
constructor(config) {
|
|
28
|
+
if (!config.clientId) throw new Error('DeevoAuth: clientId is required');
|
|
29
|
+
if (!config.clientSecret) throw new Error('DeevoAuth: clientSecret is required');
|
|
30
|
+
if (!config.redirectUri) throw new Error('DeevoAuth: redirectUri is required');
|
|
31
|
+
|
|
32
|
+
this.clientId = config.clientId;
|
|
33
|
+
this.clientSecret = config.clientSecret;
|
|
34
|
+
this.redirectUri = config.redirectUri;
|
|
35
|
+
this.authServerUrl = (config.authServerUrl || DEFAULT_AUTH_URL).replace(/\/$/, '');
|
|
36
|
+
this.scope = config.scope || 'profile email';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get the URL to redirect users to for authentication.
|
|
41
|
+
* Redirect the user's browser to this URL to start the OAuth flow.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} [options]
|
|
44
|
+
* @param {string} [options.state] - Optional state parameter for CSRF protection
|
|
45
|
+
* @returns {string} The authorization URL
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Express.js route
|
|
49
|
+
* app.get('/auth/login', (req, res) => {
|
|
50
|
+
* const url = deevo.getAuthUrl({ state: 'random-csrf-token' });
|
|
51
|
+
* res.redirect(url);
|
|
52
|
+
* });
|
|
53
|
+
*/
|
|
54
|
+
getAuthUrl(options = {}) {
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
client_id: this.clientId,
|
|
57
|
+
redirect_uri: this.redirectUri,
|
|
58
|
+
scope: this.scope,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (options.state) {
|
|
62
|
+
params.set('state', options.state);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return `${this.authServerUrl}/login?${params.toString()}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Exchange an authorization code for an access token.
|
|
70
|
+
* Call this in your callback route after the user is redirected back.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} code - The authorization code from the callback URL query params
|
|
73
|
+
* @returns {Promise<Object>} Token response with access_token, token_type, expires_in
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Express.js callback route
|
|
77
|
+
* app.get('/auth/callback', async (req, res) => {
|
|
78
|
+
* const { code } = req.query;
|
|
79
|
+
* const tokens = await deevo.exchangeCode(code);
|
|
80
|
+
* // tokens.access_token is your bearer token
|
|
81
|
+
* });
|
|
82
|
+
*/
|
|
83
|
+
async exchangeCode(code) {
|
|
84
|
+
const response = await fetch(`${this.authServerUrl}/api/oauth/token`, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'Content-Type': 'application/json' },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
grant_type: 'authorization_code',
|
|
89
|
+
code,
|
|
90
|
+
client_id: this.clientId,
|
|
91
|
+
client_secret: this.clientSecret,
|
|
92
|
+
redirect_uri: this.redirectUri,
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
const error = await response.json().catch(() => ({}));
|
|
98
|
+
throw new DeevoAuthError(
|
|
99
|
+
error.message || `Token exchange failed with status ${response.status}`,
|
|
100
|
+
error.error || 'token_exchange_failed',
|
|
101
|
+
response.status
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return response.json();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the authenticated user's profile information.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} accessToken - The access token from exchangeCode()
|
|
112
|
+
* @returns {Promise<Object>} User profile { sub, name, email, picture }
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* const user = await deevo.getUserInfo(tokens.access_token);
|
|
116
|
+
* console.log(user.email, user.name);
|
|
117
|
+
*/
|
|
118
|
+
async getUserInfo(accessToken) {
|
|
119
|
+
const response = await fetch(`${this.authServerUrl}/api/oauth/userinfo`, {
|
|
120
|
+
headers: {
|
|
121
|
+
Authorization: `Bearer ${accessToken}`,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const error = await response.json().catch(() => ({}));
|
|
127
|
+
throw new DeevoAuthError(
|
|
128
|
+
error.message || `UserInfo request failed with status ${response.status}`,
|
|
129
|
+
error.error || 'userinfo_failed',
|
|
130
|
+
response.status
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return response.json();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Complete OAuth flow in one call: exchange code and get user info.
|
|
139
|
+
* Convenience method combining exchangeCode() + getUserInfo().
|
|
140
|
+
*
|
|
141
|
+
* @param {string} code - The authorization code from the callback URL
|
|
142
|
+
* @returns {Promise<Object>} { accessToken, tokenType, expiresIn, user }
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* app.get('/auth/callback', async (req, res) => {
|
|
146
|
+
* const { accessToken, user } = await deevo.handleCallback(req.query.code);
|
|
147
|
+
* req.session.user = user;
|
|
148
|
+
* req.session.accessToken = accessToken;
|
|
149
|
+
* res.redirect('/dashboard');
|
|
150
|
+
* });
|
|
151
|
+
*/
|
|
152
|
+
async handleCallback(code) {
|
|
153
|
+
const tokens = await this.exchangeCode(code);
|
|
154
|
+
const user = await this.getUserInfo(tokens.access_token);
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
accessToken: tokens.access_token,
|
|
158
|
+
tokenType: tokens.token_type,
|
|
159
|
+
expiresIn: tokens.expires_in,
|
|
160
|
+
user,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Verify an access token and get user info (useful for API middleware).
|
|
166
|
+
*
|
|
167
|
+
* @param {string} accessToken - The Bearer token to verify
|
|
168
|
+
* @returns {Promise<Object>} User profile if valid
|
|
169
|
+
* @throws {DeevoAuthError} If the token is invalid or expired
|
|
170
|
+
*/
|
|
171
|
+
async verifyToken(accessToken) {
|
|
172
|
+
return this.getUserInfo(accessToken);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Express.js middleware for protecting routes with Deevo Auth.
|
|
178
|
+
*
|
|
179
|
+
* @param {DeevoAuth} deevoAuth - A configured DeevoAuth instance
|
|
180
|
+
* @returns {Function} Express middleware
|
|
181
|
+
*
|
|
182
|
+
* @example
|
|
183
|
+
* app.get('/api/protected', deevoMiddleware(deevo), (req, res) => {
|
|
184
|
+
* res.json({ user: req.deevoUser });
|
|
185
|
+
* });
|
|
186
|
+
*/
|
|
187
|
+
function deevoMiddleware(deevoAuth) {
|
|
188
|
+
return async (req, res, next) => {
|
|
189
|
+
const authHeader = req.headers.authorization;
|
|
190
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
191
|
+
return res.status(401).json({ error: 'Missing or invalid authorization header' });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const token = authHeader.split(' ')[1];
|
|
195
|
+
try {
|
|
196
|
+
const user = await deevoAuth.verifyToken(token);
|
|
197
|
+
req.deevoUser = user;
|
|
198
|
+
next();
|
|
199
|
+
} catch (error) {
|
|
200
|
+
return res.status(401).json({ error: 'Invalid or expired token' });
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Custom error class for Deevo Auth errors.
|
|
207
|
+
*/
|
|
208
|
+
class DeevoAuthError extends Error {
|
|
209
|
+
constructor(message, code, statusCode) {
|
|
210
|
+
super(message);
|
|
211
|
+
this.name = 'DeevoAuthError';
|
|
212
|
+
this.code = code;
|
|
213
|
+
this.statusCode = statusCode;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Export for CommonJS
|
|
218
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
219
|
+
module.exports = { DeevoAuth, deevoMiddleware, DeevoAuthError };
|
|
220
|
+
module.exports.DeevoAuth = DeevoAuth;
|
|
221
|
+
module.exports.deevoMiddleware = deevoMiddleware;
|
|
222
|
+
module.exports.DeevoAuthError = DeevoAuthError;
|
|
223
|
+
module.exports.default = DeevoAuth;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Export for ESM
|
|
227
|
+
export { DeevoAuth, deevoMiddleware, DeevoAuthError };
|
|
228
|
+
export default DeevoAuth;
|