@joonweb/joonweb-sdk 1.0.0
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/index.js +8 -0
- package/package.json +60 -0
- package/src/Auth/Auth.js +193 -0
- package/src/Auth/OAuth.js +135 -0
- package/src/Auth/SessionManager.js +145 -0
- package/src/Auth/session/Session.js +46 -0
- package/src/Auth/session/StorageInterface.js +48 -0
- package/src/Auth/session/storage/MemoryStorage.js +34 -0
- package/src/Auth/session/storage/MongoDBStorage.js +107 -0
- package/src/Auth/session/storage/MySQLStorage.js +281 -0
- package/src/Auth/session/storage/PostgreSQLStorage.js +372 -0
- package/src/Auth/session/storage/RedisStorage.js +133 -0
- package/src/Auth/session/storage/SQLiteStorage.js +133 -0
- package/src/Auth/session/storage/index.js +46 -0
- package/src/Clients/BaseClient.js +66 -0
- package/src/Context.js +82 -0
- package/src/Helper.js +92 -0
- package/src/JoonWebAPI.js +81 -0
- package/src/Resources/BaseResource.js +32 -0
- package/src/Resources/Customer.js +34 -0
- package/src/Resources/Order.js +18 -0
- package/src/Resources/Product.js +30 -0
- package/src/Resources/Site.js +11 -0
- package/src/Resources/Theme.js +7 -0
- package/src/Resources/Webhook.js +30 -0
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@joonweb/joonweb-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official Node.js SDK for JoonWeb API",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"test:watch": "jest --watch",
|
|
12
|
+
"lint": "eslint src/",
|
|
13
|
+
"format": "prettier --write \"src/**/*.js\"",
|
|
14
|
+
"example": "node examples/express-app.js",
|
|
15
|
+
"prepare": "npm run build",
|
|
16
|
+
"build": "echo 'No build step required for JavaScript SDK'"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"joonweb",
|
|
20
|
+
"api",
|
|
21
|
+
"sdk",
|
|
22
|
+
"ecommerce",
|
|
23
|
+
"oauth",
|
|
24
|
+
"rest-api"
|
|
25
|
+
],
|
|
26
|
+
"author": "Joonweb <business@jooncorporation.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/JoonWebdotcom/joonweb-node-sdk.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/JoonWebdotcom/joonweb-node-sdk/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/JoonWebdotcom/joonweb-node-sdk#readme",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=14.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"axios": "^1.6.0"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"jest": "^29.7.0",
|
|
44
|
+
"eslint": "^8.53.0",
|
|
45
|
+
"prettier": "^3.0.3",
|
|
46
|
+
"dotenv": "^16.3.0",
|
|
47
|
+
"express": "^4.18.2",
|
|
48
|
+
"express-session": "^1.17.3"
|
|
49
|
+
},
|
|
50
|
+
"optionalDependencies": {
|
|
51
|
+
"sqlite3": "^5.1.7",
|
|
52
|
+
"mongodb": "^6.3.0",
|
|
53
|
+
"redis": "^4.6.13"
|
|
54
|
+
},
|
|
55
|
+
"files": [
|
|
56
|
+
"src/",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"README.md"
|
|
59
|
+
]
|
|
60
|
+
}
|
package/src/Auth/Auth.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const Helper = require('../Helper');
|
|
2
|
+
const Context = require('../Context');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
class Auth {
|
|
6
|
+
static STATE_COOKIE_NAME = 'joonweb_oauth_state';
|
|
7
|
+
static STATE_SIG_COOKIE_NAME = 'joonweb_oauth_state_sig';
|
|
8
|
+
static SESSION_COOKIE_NAME = 'joonweb_session_id';
|
|
9
|
+
static SESSION_SIG_COOKIE_NAME = 'joonweb_session_id_sig';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Begin OAuth flow
|
|
13
|
+
*/
|
|
14
|
+
static beginAuth(site, redirectUri, scopes, isOnline = true) {
|
|
15
|
+
Context.throwIfUninitialized();
|
|
16
|
+
|
|
17
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
18
|
+
const scope = Array.isArray(scopes) ? scopes.join(',') : scopes;
|
|
19
|
+
|
|
20
|
+
// Generate authorization URL
|
|
21
|
+
const authUrl = `https://${site}/admin/oauth/authorize?` + new URLSearchParams({
|
|
22
|
+
client_id: Context.API_KEY,
|
|
23
|
+
scope: scope,
|
|
24
|
+
redirect_uri: redirectUri,
|
|
25
|
+
state: state,
|
|
26
|
+
'grant_options[]': isOnline ? 'per-user' : ''
|
|
27
|
+
}).toString();
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
url: authUrl,
|
|
31
|
+
state: state
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate OAuth callback
|
|
37
|
+
*/
|
|
38
|
+
static async validateAuthCallback(query, cookies, setCookieFunction = null) {
|
|
39
|
+
Context.throwIfUninitialized();
|
|
40
|
+
|
|
41
|
+
const { site, code, state, hmac, timestamp } = query;
|
|
42
|
+
|
|
43
|
+
// Validate HMAC
|
|
44
|
+
if (!Helper.verifyHmac(query, Context.API_SECRET_KEY)) {
|
|
45
|
+
throw new Error('Invalid HMAC');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Validate state from cookies
|
|
49
|
+
const stateCookie = this._getCookie(cookies, this.STATE_COOKIE_NAME, this.STATE_SIG_COOKIE_NAME);
|
|
50
|
+
if (!stateCookie || stateCookie !== state) {
|
|
51
|
+
throw new Error('Invalid OAuth state');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Exchange code for token
|
|
55
|
+
const tokenData = await this._exchangeCodeForToken(code, site);
|
|
56
|
+
|
|
57
|
+
// Create session
|
|
58
|
+
const session = {
|
|
59
|
+
site: site,
|
|
60
|
+
accessToken: tokenData.access_token,
|
|
61
|
+
scope: tokenData.scope,
|
|
62
|
+
expiresAt: tokenData.expires_in ? Date.now() + (tokenData.expires_in * 1000) : null,
|
|
63
|
+
isOnline: !!tokenData.associated_user,
|
|
64
|
+
user: tokenData.associated_user || null
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Store session if storage is configured
|
|
68
|
+
if (Context.SESSION_STORAGE) {
|
|
69
|
+
await Context.SESSION_STORAGE.storeSession(session);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Set session cookie for non-embedded apps
|
|
73
|
+
if (!Context.IS_EMBEDDED_APP && setCookieFunction) {
|
|
74
|
+
this._setSessionCookie(session, setCookieFunction);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return session;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get current session from request
|
|
82
|
+
*/
|
|
83
|
+
static async getCurrentSession(headers, cookies) {
|
|
84
|
+
Context.throwIfUninitialized();
|
|
85
|
+
|
|
86
|
+
if (Context.IS_EMBEDDED_APP) {
|
|
87
|
+
// Get session from JWT (embedded app)
|
|
88
|
+
const authHeader = headers['authorization'];
|
|
89
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const jwt = authHeader.split(' ')[1];
|
|
94
|
+
const payload = Helper.decodeSessionToken(jwt);
|
|
95
|
+
|
|
96
|
+
if (!payload) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Extract site from JWT
|
|
101
|
+
const site = payload.dest ? payload.dest.replace('https://', '') : null;
|
|
102
|
+
if (!site) {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Load session from storage
|
|
107
|
+
if (Context.SESSION_STORAGE) {
|
|
108
|
+
return await Context.SESSION_STORAGE.loadSession(site);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
} else {
|
|
112
|
+
// Get session from cookies (non-embedded)
|
|
113
|
+
const sessionId = this._getCookie(cookies, this.SESSION_COOKIE_NAME, this.SESSION_SIG_COOKIE_NAME);
|
|
114
|
+
if (!sessionId || !Context.SESSION_STORAGE) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return await Context.SESSION_STORAGE.loadSessionById(sessionId);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Exchange authorization code for access token
|
|
126
|
+
*/
|
|
127
|
+
static async _exchangeCodeForToken(code, site) {
|
|
128
|
+
const url = `https://${site}/admin/oauth/access_token`;
|
|
129
|
+
|
|
130
|
+
const response = await fetch(url, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: {
|
|
133
|
+
'Content-Type': 'application/json',
|
|
134
|
+
'Accept': 'application/json'
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
client_id: Context.API_KEY,
|
|
138
|
+
client_secret: Context.API_SECRET_KEY,
|
|
139
|
+
code: code
|
|
140
|
+
})
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`Token exchange failed: ${response.status}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return await response.json();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get and validate cookie
|
|
152
|
+
*/
|
|
153
|
+
static _getCookie(cookies, name, sigName) {
|
|
154
|
+
const cookieValue = cookies[name];
|
|
155
|
+
const signature = cookies[sigName];
|
|
156
|
+
|
|
157
|
+
if (!cookieValue || !signature) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Validate signature
|
|
162
|
+
const expectedSig = crypto
|
|
163
|
+
.createHmac('sha256', Context.API_SECRET_KEY)
|
|
164
|
+
.update(cookieValue)
|
|
165
|
+
.digest('hex');
|
|
166
|
+
|
|
167
|
+
if (signature !== expectedSig) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return cookieValue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Set session cookie with signature
|
|
176
|
+
*/
|
|
177
|
+
static _setSessionCookie(session, setCookieFunction) {
|
|
178
|
+
const sessionId = session.id || crypto.randomBytes(16).toString('hex');
|
|
179
|
+
const signature = crypto
|
|
180
|
+
.createHmac('sha256', Context.API_SECRET_KEY)
|
|
181
|
+
.update(sessionId)
|
|
182
|
+
.digest('hex');
|
|
183
|
+
|
|
184
|
+
const expires = session.expiresAt ? new Date(session.expiresAt) : undefined;
|
|
185
|
+
|
|
186
|
+
if (setCookieFunction) {
|
|
187
|
+
setCookieFunction(this.SESSION_COOKIE_NAME, sessionId, { expires, httpOnly: true });
|
|
188
|
+
setCookieFunction(this.SESSION_SIG_COOKIE_NAME, signature, { expires, httpOnly: true });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = Auth;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
const Helper = require('../Helper');
|
|
2
|
+
const SessionManager = require('./SessionManager');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
class OAuth {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.clientId = config.clientId || process.env.JOONWEB_CLIENT_ID || 'test_client';
|
|
8
|
+
this.clientSecret = config.clientSecret || process.env.JOONWEB_CLIENT_SECRET || 'test_secret';
|
|
9
|
+
this.redirectUri = config.redirectUri || process.env.JOONWEB_REDIRECT_URI || 'http://localhost:3000/auth/callback';
|
|
10
|
+
this.scopes = config.scopes || ['read_products', 'write_products'];
|
|
11
|
+
this.sessionManager = config.sessionManager || new SessionManager();
|
|
12
|
+
this.appSlug = config.appSlug || process.env.JOONWEB_CLIENT_ID || 'test-app';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate OAuth URL
|
|
17
|
+
*/
|
|
18
|
+
beginAuth(siteDomain) {
|
|
19
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
20
|
+
|
|
21
|
+
const params = new URLSearchParams({
|
|
22
|
+
client_id: this.clientId,
|
|
23
|
+
scope: this.scopes.join(','),
|
|
24
|
+
redirect_uri: this.redirectUri,
|
|
25
|
+
state: state,
|
|
26
|
+
site: siteDomain
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
url: `https://accounts.joonweb.com/oauth/authorize?${params.toString()}`,
|
|
31
|
+
state: state
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async exchangeCodeForToken(code, siteDomain) {
|
|
35
|
+
const JoonWebAPI = require('../JoonWebAPI');
|
|
36
|
+
const api = new JoonWebAPI();
|
|
37
|
+
|
|
38
|
+
const tokenData = await api.exchangeCodeForToken(code, siteDomain, {
|
|
39
|
+
client_id: this.clientId,
|
|
40
|
+
client_secret: this.clientSecret,
|
|
41
|
+
redirect_uri: this.redirectUri
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return tokenData;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Validate OAuth callback
|
|
48
|
+
*/
|
|
49
|
+
async validateAuthCallback(query, sessionState) {
|
|
50
|
+
|
|
51
|
+
// For testing, skip HMAC verification
|
|
52
|
+
if (!this.verifyHmac(query)) {
|
|
53
|
+
throw new Error('Invalid HMAC signature');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { code, site } = query;
|
|
57
|
+
|
|
58
|
+
// Create API instance
|
|
59
|
+
const JoonWebAPI = require('../JoonWebAPI');
|
|
60
|
+
const api = new JoonWebAPI();
|
|
61
|
+
|
|
62
|
+
// Exchange code for token
|
|
63
|
+
const tokenData = await api.exchangeCodeForToken(code, site);
|
|
64
|
+
|
|
65
|
+
// Start session
|
|
66
|
+
await this.sessionManager.startSession(site, tokenData);
|
|
67
|
+
|
|
68
|
+
// Set API credentials
|
|
69
|
+
api.setAccessToken(tokenData.access_token)
|
|
70
|
+
.setSiteDomain(site);
|
|
71
|
+
|
|
72
|
+
// Get site info
|
|
73
|
+
const siteInfo = await api.site.get();
|
|
74
|
+
|
|
75
|
+
// Save data
|
|
76
|
+
await this.sessionManager.saveAppData(site, 'site_info', siteInfo.site);
|
|
77
|
+
|
|
78
|
+
await this.sessionManager.trackAnalytics(site, 'installation_completed', {
|
|
79
|
+
site_name: siteInfo.site.name,
|
|
80
|
+
email: siteInfo.site.email
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
site: site,
|
|
85
|
+
accessToken: tokenData.access_token,
|
|
86
|
+
scope: tokenData.scope,
|
|
87
|
+
expiresAt: tokenData.expires_in ? Date.now() + (tokenData.expires_in * 1000) : null,
|
|
88
|
+
user: tokenData.associated_user || null,
|
|
89
|
+
siteInfo: siteInfo.site
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
verifyHmac(params) {
|
|
94
|
+
// For testing, always return true
|
|
95
|
+
return true;
|
|
96
|
+
|
|
97
|
+
// Real implementation would verify HMAC here
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getEmbedUrl(siteDomain, siteHash = '', app_slug = null) {
|
|
101
|
+
return `https://accounts.joonweb.com/site/?sitehash=${siteHash}&apps&${encodeURIComponent(app_slug)}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Complete OAuth flow handler
|
|
106
|
+
*/
|
|
107
|
+
async handleCallback(req, res) {
|
|
108
|
+
try {
|
|
109
|
+
|
|
110
|
+
const session = await this.validateAuthCallback(req.query, req.session.oauth_state);
|
|
111
|
+
|
|
112
|
+
// Build embed URL
|
|
113
|
+
const embedUrl = this.getEmbedUrl(session.site, req.query.site_hash, req.query.app_slug);
|
|
114
|
+
// Redirect to embedded app
|
|
115
|
+
res.redirect(embedUrl);
|
|
116
|
+
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('OAuth Error:', error);
|
|
119
|
+
|
|
120
|
+
if (req.query.site) {
|
|
121
|
+
await this.sessionManager.trackAnalytics(req.query.site, 'installation_failed', {
|
|
122
|
+
error: error.message
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
res.status(400).send(`
|
|
127
|
+
<h1>❌ Installation Failed</h1>
|
|
128
|
+
<p>Error: ${error.message}</p>
|
|
129
|
+
<p><a href="/auth?site=${req.query.site || ''}">Try Again</a></p>
|
|
130
|
+
`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = OAuth;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// src/Auth/SessionManager.js
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
class SessionManager {
|
|
5
|
+
constructor(storageAdapter = null) {
|
|
6
|
+
// Use provided adapter or default to MemoryStorage
|
|
7
|
+
if (storageAdapter) {
|
|
8
|
+
this.storage = storageAdapter;
|
|
9
|
+
} else {
|
|
10
|
+
const { MemoryStorage } = require('./session/storage');
|
|
11
|
+
this.storage = new MemoryStorage();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async startSession(siteDomain, tokenData, additionalData = {}) {
|
|
16
|
+
const session = {
|
|
17
|
+
site_domain: siteDomain,
|
|
18
|
+
access_token: tokenData.access_token,
|
|
19
|
+
scope: tokenData.scope,
|
|
20
|
+
expires_at: tokenData.expires_in ? Date.now() + (tokenData.expires_in * 1000) : null,
|
|
21
|
+
user: tokenData.associated_user || null,
|
|
22
|
+
authenticated_at: Date.now(),
|
|
23
|
+
...additionalData
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await this.storage.store(siteDomain, session);
|
|
27
|
+
return session;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async isAuthenticated(siteDomain) {
|
|
31
|
+
const session = await this.storage.load(siteDomain);
|
|
32
|
+
if (!session) return false;
|
|
33
|
+
|
|
34
|
+
if (session.expires_at && session.expires_at < Date.now()) {
|
|
35
|
+
await this.destroySession(siteDomain);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getAccessToken(siteDomain) {
|
|
43
|
+
const session = await this.storage.load(siteDomain);
|
|
44
|
+
return session ? session.access_token : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getSessionData(siteDomain) {
|
|
48
|
+
return await this.storage.load(siteDomain);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getSiteDomain(siteDomain) {
|
|
52
|
+
return siteDomain; // This method seems redundant
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async saveAppData(siteDomain, key, data) {
|
|
56
|
+
// Load existing session
|
|
57
|
+
const session = await this.getSessionData(siteDomain);
|
|
58
|
+
if (!session) return false;
|
|
59
|
+
|
|
60
|
+
// Add metadata
|
|
61
|
+
session.metadata = session.metadata || {};
|
|
62
|
+
session.metadata[key] = data;
|
|
63
|
+
|
|
64
|
+
// Save back
|
|
65
|
+
await this.storage.store(siteDomain, session);
|
|
66
|
+
console.log(`✅ Saved ${key} for ${siteDomain}`);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async trackAnalytics(siteDomain, event, data = {}) {
|
|
71
|
+
// Load existing session
|
|
72
|
+
const session = await this.getSessionData(siteDomain);
|
|
73
|
+
if (!session) return false;
|
|
74
|
+
|
|
75
|
+
// Add analytics data
|
|
76
|
+
session.metadata = session.metadata || {};
|
|
77
|
+
session.metadata.analytics = session.metadata.analytics || [];
|
|
78
|
+
session.metadata.analytics.push({
|
|
79
|
+
event,
|
|
80
|
+
data,
|
|
81
|
+
timestamp: Date.now()
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Save back
|
|
85
|
+
await this.storage.store(siteDomain, session);
|
|
86
|
+
console.log(`📊 Tracked ${event} for ${siteDomain}`);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
isEmbeddedRequest(headers) {
|
|
91
|
+
return (
|
|
92
|
+
(headers['sec-fetch-dest'] === 'iframe') ||
|
|
93
|
+
(headers.referer && headers.referer.includes('joonweb.com'))
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async destroySession(siteDomain) {
|
|
98
|
+
return await this.storage.delete(siteDomain);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// New methods for better session management
|
|
102
|
+
async refreshSession(siteDomain, newTokenData) {
|
|
103
|
+
const existingSession = await this.getSessionData(siteDomain);
|
|
104
|
+
if (!existingSession) return null;
|
|
105
|
+
|
|
106
|
+
const updatedSession = {
|
|
107
|
+
...existingSession,
|
|
108
|
+
access_token: newTokenData.access_token,
|
|
109
|
+
expires_at: newTokenData.expires_in ? Date.now() + (newTokenData.expires_in * 1000) : null,
|
|
110
|
+
updated_at: Date.now()
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await this.storage.store(siteDomain, updatedSession);
|
|
114
|
+
return updatedSession;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async listAllSessions() {
|
|
118
|
+
return await this.storage.list();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async clearAllSessions() {
|
|
122
|
+
return await this.storage.clear();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async getSessionCount() {
|
|
126
|
+
const sessions = await this.listAllSessions();
|
|
127
|
+
return sessions.length;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Utility method to get active sessions
|
|
131
|
+
async getActiveSessions() {
|
|
132
|
+
const allSessions = await this.listAllSessions();
|
|
133
|
+
const activeSessions = [];
|
|
134
|
+
|
|
135
|
+
for (const session of allSessions) {
|
|
136
|
+
if (await this.isAuthenticated(session.site_domain)) {
|
|
137
|
+
activeSessions.push(session);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return activeSessions;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = SessionManager;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
class Session {
|
|
2
|
+
constructor(id, siteDomain, accessToken) {
|
|
3
|
+
this.id = id;
|
|
4
|
+
this.site_domain = siteDomain;
|
|
5
|
+
this.access_token = accessToken;
|
|
6
|
+
this.scope = '';
|
|
7
|
+
this.user = null;
|
|
8
|
+
this.expires_at = null;
|
|
9
|
+
this.created_at = Date.now();
|
|
10
|
+
this.updated_at = Date.now();
|
|
11
|
+
this.is_online = false;
|
|
12
|
+
this.online_access_info = {};
|
|
13
|
+
this.state = '';
|
|
14
|
+
this.metadata = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
isValid() {
|
|
18
|
+
if (!this.expires_at) return true;
|
|
19
|
+
return this.expires_at > Date.now();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
toJSON() {
|
|
23
|
+
return {
|
|
24
|
+
id: this.id,
|
|
25
|
+
site_domain: this.site_domain,
|
|
26
|
+
access_token: this.access_token,
|
|
27
|
+
scope: this.scope,
|
|
28
|
+
user: this.user,
|
|
29
|
+
expires_at: this.expires_at,
|
|
30
|
+
created_at: this.created_at,
|
|
31
|
+
updated_at: this.updated_at,
|
|
32
|
+
is_online: this.is_online,
|
|
33
|
+
online_access_info: this.online_access_info,
|
|
34
|
+
state: this.state,
|
|
35
|
+
metadata: this.metadata
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static fromJSON(data) {
|
|
40
|
+
const session = new Session(data.id, data.site_domain, data.access_token);
|
|
41
|
+
Object.assign(session, data);
|
|
42
|
+
return session;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = Session;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
class StorageInterface {
|
|
2
|
+
/**
|
|
3
|
+
* Store a JoonWeb session
|
|
4
|
+
* @param {Session} session
|
|
5
|
+
* @returns {Promise<boolean>}
|
|
6
|
+
*/
|
|
7
|
+
async store(session) {
|
|
8
|
+
throw new Error('Method not implemented: store');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load a session by ID
|
|
13
|
+
* @param {string} sessionId
|
|
14
|
+
* @returns {Promise<Session|null>}
|
|
15
|
+
*/
|
|
16
|
+
async load(sessionId) {
|
|
17
|
+
throw new Error('Method not implemented: load');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Delete a session
|
|
22
|
+
* @param {string} sessionId
|
|
23
|
+
* @returns {Promise<boolean>}
|
|
24
|
+
*/
|
|
25
|
+
async delete(sessionId) {
|
|
26
|
+
throw new Error('Method not implemented: delete');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Find sessions by site domain
|
|
31
|
+
* @param {string} siteDomain
|
|
32
|
+
* @returns {Promise<Session[]>}
|
|
33
|
+
*/
|
|
34
|
+
async findBySiteDomain(siteDomain) {
|
|
35
|
+
throw new Error('Method not implemented: findBySiteDomain');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Find sessions by query
|
|
40
|
+
* @param {Object} query
|
|
41
|
+
* @returns {Promise<Session[]>}
|
|
42
|
+
*/
|
|
43
|
+
async findByQuery(query) {
|
|
44
|
+
throw new Error('Method not implemented: findByQuery');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = StorageInterface;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/session/storage/MemoryStorage.js
|
|
2
|
+
class MemoryStorage {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.sessions = new Map();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
async store(siteDomain, session) {
|
|
8
|
+
this.sessions.set(siteDomain, session);
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async load(siteDomain) {
|
|
13
|
+
return this.sessions.get(siteDomain) || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async delete(siteDomain) {
|
|
17
|
+
return this.sessions.delete(siteDomain);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async list() {
|
|
21
|
+
return Array.from(this.sessions.values());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async clear() {
|
|
25
|
+
this.sessions.clear();
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async count() {
|
|
30
|
+
return this.sessions.size;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = MemoryStorage;
|