@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.
@@ -0,0 +1,372 @@
1
+ // src/Auth/session/storage/PostgreSQLStorage.js
2
+ let pg;
3
+
4
+ try {
5
+ pg = require('pg');
6
+ } catch (error) {
7
+ // pg package not installed
8
+ pg = null;
9
+ }
10
+
11
+ class PostgreSQLStorage {
12
+ constructor(options = {}) {
13
+ if (!pg) {
14
+ throw new Error('pg package not installed. Run: npm install pg');
15
+ }
16
+
17
+ this.config = {
18
+ host: options.host || 'localhost',
19
+ port: options.port || 5432,
20
+ user: options.user || 'postgres',
21
+ password: options.password || '',
22
+ database: options.database || 'joonweb_sessions',
23
+ max: options.max || 20,
24
+ idleTimeoutMillis: options.idleTimeoutMillis || 30000,
25
+ connectionTimeoutMillis: options.connectionTimeoutMillis || 2000,
26
+ };
27
+
28
+ this.tableName = options.tableName || 'sessions';
29
+ this.schema = options.schema || 'public';
30
+ this.pool = null;
31
+ this.initialized = false;
32
+ }
33
+
34
+ async initDatabase() {
35
+ if (!this.pool) {
36
+ this.pool = new pg.Pool(this.config);
37
+
38
+ // Test connection
39
+ const client = await this.pool.connect();
40
+ try {
41
+ // Create schema if it doesn't exist
42
+ await client.query(`CREATE SCHEMA IF NOT EXISTS ${this.schema}`);
43
+
44
+ // Create table if not exists
45
+ await this.createTable(client);
46
+
47
+ // Create cleanup job for expired sessions
48
+ await this.createCleanupTrigger(client);
49
+
50
+ this.initialized = true;
51
+ console.log(`✅ PostgreSQL storage initialized (${this.config.database}.${this.schema}.${this.tableName})`);
52
+ } finally {
53
+ client.release();
54
+ }
55
+ }
56
+ }
57
+
58
+ async createTable(client) {
59
+ await client.query(`
60
+ CREATE TABLE IF NOT EXISTS ${this.schema}.${this.tableName} (
61
+ id VARCHAR(255) PRIMARY KEY,
62
+ site_domain VARCHAR(255) NOT NULL,
63
+ access_token TEXT NOT NULL,
64
+ scope TEXT,
65
+ user_data JSONB,
66
+ expires_at BIGINT,
67
+ authenticated_at BIGINT NOT NULL,
68
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
69
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
70
+ is_online BOOLEAN DEFAULT FALSE,
71
+ online_access_info JSONB,
72
+ state VARCHAR(255),
73
+ metadata JSONB,
74
+
75
+ -- Indexes for performance
76
+ CONSTRAINT unique_site_domain UNIQUE (site_domain)
77
+ )
78
+ `);
79
+
80
+ // Create indexes
81
+ await client.query(`
82
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires_at
83
+ ON ${this.schema}.${this.tableName} (expires_at)
84
+ WHERE expires_at IS NOT NULL
85
+ `);
86
+
87
+ await client.query(`
88
+ CREATE INDEX IF NOT EXISTS idx_sessions_created_at
89
+ ON ${this.schema}.${this.tableName} (created_at)
90
+ `);
91
+
92
+ await client.query(`
93
+ CREATE INDEX IF NOT EXISTS idx_sessions_is_online
94
+ ON ${this.schema}.${this.tableName} (is_online)
95
+ `);
96
+ }
97
+
98
+ async createCleanupTrigger(client) {
99
+ // Create function to cleanup expired sessions
100
+ await client.query(`
101
+ CREATE OR REPLACE FUNCTION ${this.schema}.cleanup_expired_sessions()
102
+ RETURNS void AS $$
103
+ BEGIN
104
+ DELETE FROM ${this.schema}.${this.tableName}
105
+ WHERE expires_at IS NOT NULL
106
+ AND expires_at < EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000;
107
+ END;
108
+ $$ LANGUAGE plpgsql
109
+ `);
110
+ }
111
+
112
+ async ensureConnected() {
113
+ if (!this.initialized) {
114
+ await this.initDatabase();
115
+ }
116
+ }
117
+
118
+ async store(siteDomain, session) {
119
+ await this.ensureConnected();
120
+
121
+ const client = await this.pool.connect();
122
+ try {
123
+ // Generate a unique ID for the session
124
+ const crypto = require('crypto');
125
+ const sessionId = crypto.randomBytes(16).toString('hex');
126
+
127
+ await client.query(
128
+ `INSERT INTO ${this.schema}.${this.tableName}
129
+ (id, site_domain, access_token, scope, user_data, expires_at,
130
+ authenticated_at, is_online, online_access_info, state, metadata)
131
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
132
+ ON CONFLICT (site_domain)
133
+ DO UPDATE SET
134
+ access_token = EXCLUDED.access_token,
135
+ scope = EXCLUDED.scope,
136
+ user_data = EXCLUDED.user_data,
137
+ expires_at = EXCLUDED.expires_at,
138
+ is_online = EXCLUDED.is_online,
139
+ online_access_info = EXCLUDED.online_access_info,
140
+ state = EXCLUDED.state,
141
+ metadata = EXCLUDED.metadata,
142
+ updated_at = CURRENT_TIMESTAMP`,
143
+ [
144
+ sessionId,
145
+ siteDomain,
146
+ session.access_token,
147
+ session.scope,
148
+ JSON.stringify(session.user || {}),
149
+ session.expires_at,
150
+ session.authenticated_at,
151
+ session.is_online || false,
152
+ JSON.stringify(session.online_access_info || {}),
153
+ session.state || '',
154
+ JSON.stringify(session.metadata || {})
155
+ ]
156
+ );
157
+
158
+ return true;
159
+ } finally {
160
+ client.release();
161
+ }
162
+ }
163
+
164
+ async load(siteDomain) {
165
+ await this.ensureConnected();
166
+
167
+ const client = await this.pool.connect();
168
+ try {
169
+ const result = await client.query(
170
+ `SELECT * FROM ${this.schema}.${this.tableName}
171
+ WHERE site_domain = $1
172
+ ORDER BY updated_at DESC LIMIT 1`,
173
+ [siteDomain]
174
+ );
175
+
176
+ if (result.rows.length === 0) {
177
+ return null;
178
+ }
179
+
180
+ const row = result.rows[0];
181
+
182
+ // Parse JSONB fields (PostgreSQL returns them as objects)
183
+ const session = {
184
+ id: row.id,
185
+ site_domain: row.site_domain,
186
+ access_token: row.access_token,
187
+ scope: row.scope,
188
+ user: row.user_data || null,
189
+ expires_at: row.expires_at,
190
+ authenticated_at: row.authenticated_at,
191
+ is_online: Boolean(row.is_online),
192
+ online_access_info: row.online_access_info || {},
193
+ state: row.state,
194
+ metadata: row.metadata || {},
195
+ created_at: row.created_at,
196
+ updated_at: row.updated_at
197
+ };
198
+
199
+ return session;
200
+ } finally {
201
+ client.release();
202
+ }
203
+ }
204
+
205
+ async delete(siteDomain) {
206
+ await this.ensureConnected();
207
+
208
+ const client = await this.pool.connect();
209
+ try {
210
+ const result = await client.query(
211
+ `DELETE FROM ${this.schema}.${this.tableName} WHERE site_domain = $1`,
212
+ [siteDomain]
213
+ );
214
+
215
+ return result.rowCount > 0;
216
+ } finally {
217
+ client.release();
218
+ }
219
+ }
220
+
221
+ async list() {
222
+ await this.ensureConnected();
223
+
224
+ const client = await this.pool.connect();
225
+ try {
226
+ const result = await client.query(
227
+ `SELECT * FROM ${this.schema}.${this.tableName}
228
+ ORDER BY updated_at DESC`
229
+ );
230
+
231
+ return result.rows.map(row => ({
232
+ id: row.id,
233
+ site_domain: row.site_domain,
234
+ access_token: row.access_token,
235
+ scope: row.scope,
236
+ user: row.user_data || null,
237
+ expires_at: row.expires_at,
238
+ authenticated_at: row.authenticated_at,
239
+ is_online: Boolean(row.is_online),
240
+ online_access_info: row.online_access_info || {},
241
+ state: row.state,
242
+ metadata: row.metadata || {},
243
+ created_at: row.created_at,
244
+ updated_at: row.updated_at
245
+ }));
246
+ } finally {
247
+ client.release();
248
+ }
249
+ }
250
+
251
+ async clear() {
252
+ await this.ensureConnected();
253
+
254
+ const client = await this.pool.connect();
255
+ try {
256
+ await client.query(`TRUNCATE TABLE ${this.schema}.${this.tableName} RESTART IDENTITY`);
257
+ return true;
258
+ } finally {
259
+ client.release();
260
+ }
261
+ }
262
+
263
+ async close() {
264
+ if (this.pool) {
265
+ await this.pool.end();
266
+ }
267
+ return true;
268
+ }
269
+
270
+ async cleanupExpiredSessions() {
271
+ await this.ensureConnected();
272
+
273
+ const client = await this.pool.connect();
274
+ try {
275
+ const result = await client.query(
276
+ `DELETE FROM ${this.schema}.${this.tableName}
277
+ WHERE expires_at IS NOT NULL
278
+ AND expires_at < EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000`
279
+ );
280
+
281
+ if (result.rowCount > 0) {
282
+ console.log(`🧹 Cleaned up ${result.rowCount} expired sessions from PostgreSQL`);
283
+ }
284
+
285
+ return result.rowCount;
286
+ } finally {
287
+ client.release();
288
+ }
289
+ }
290
+
291
+ async getStats() {
292
+ await this.ensureConnected();
293
+
294
+ const client = await this.pool.connect();
295
+ try {
296
+ const result = await client.query(`
297
+ SELECT
298
+ COUNT(*) as total_sessions,
299
+ COUNT(CASE WHEN expires_at IS NULL OR expires_at > EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 THEN 1 END) as active_sessions,
300
+ COUNT(CASE WHEN is_online = TRUE THEN 1 END) as online_sessions,
301
+ MIN(created_at) as oldest_session,
302
+ MAX(updated_at) as latest_activity,
303
+ pg_size_pretty(pg_total_relation_size('${this.schema}.${this.tableName}')) as table_size
304
+ FROM ${this.schema}.${this.tableName}
305
+ `);
306
+
307
+ return result.rows[0];
308
+ } finally {
309
+ client.release();
310
+ }
311
+ }
312
+
313
+ async healthCheck() {
314
+ try {
315
+ await this.ensureConnected();
316
+ const client = await this.pool.connect();
317
+ await client.query('SELECT 1');
318
+ client.release();
319
+
320
+ return {
321
+ status: 'healthy',
322
+ storage: 'postgresql',
323
+ database: this.config.database,
324
+ table: `${this.schema}.${this.tableName}`
325
+ };
326
+ } catch (error) {
327
+ return {
328
+ status: 'unhealthy',
329
+ storage: 'postgresql',
330
+ error: error.message
331
+ };
332
+ }
333
+ }
334
+
335
+ // PostgreSQL-specific: Full-text search
336
+ async searchSessions(searchTerm) {
337
+ await this.ensureConnected();
338
+
339
+ const client = await this.pool.connect();
340
+ try {
341
+ const result = await client.query(
342
+ `SELECT * FROM ${this.schema}.${this.tableName}
343
+ WHERE site_domain ILIKE $1
344
+ OR access_token ILIKE $1
345
+ OR scope ILIKE $1
346
+ OR (metadata::text) ILIKE $1
347
+ ORDER BY updated_at DESC`,
348
+ [`%${searchTerm}%`]
349
+ );
350
+
351
+ return result.rows.map(row => ({
352
+ id: row.id,
353
+ site_domain: row.site_domain,
354
+ access_token: row.access_token,
355
+ scope: row.scope,
356
+ user: row.user_data || null,
357
+ expires_at: row.expires_at,
358
+ authenticated_at: row.authenticated_at,
359
+ is_online: Boolean(row.is_online),
360
+ online_access_info: row.online_access_info || {},
361
+ state: row.state,
362
+ metadata: row.metadata || {},
363
+ created_at: row.created_at,
364
+ updated_at: row.updated_at
365
+ }));
366
+ } finally {
367
+ client.release();
368
+ }
369
+ }
370
+ }
371
+
372
+ module.exports = PostgreSQLStorage;
@@ -0,0 +1,133 @@
1
+ // src/Auth/session/storage/RedisStorage.js
2
+ let redis;
3
+
4
+ try {
5
+ redis = require('redis');
6
+ } catch (error) {
7
+ // redis not installed
8
+ redis = null;
9
+ }
10
+
11
+ class RedisStorage {
12
+ constructor(options = {}) {
13
+ if (!redis) {
14
+ throw new Error('redis package not installed. Run: npm install redis');
15
+ }
16
+
17
+ this.url = options.url || 'redis://localhost:6379';
18
+ this.prefix = options.prefix || 'joonweb:session:';
19
+ this.client = null;
20
+ }
21
+
22
+ async connect() {
23
+ if (!this.client) {
24
+ this.client = redis.createClient({ url: this.url });
25
+ await this.client.connect();
26
+
27
+ // Handle connection errors
28
+ this.client.on('error', (err) => {
29
+ console.error('Redis connection error:', err);
30
+ });
31
+ }
32
+ return this.client;
33
+ }
34
+
35
+ async ensureConnected() {
36
+ if (!this.client) {
37
+ await this.connect();
38
+ }
39
+ }
40
+
41
+ async store(siteDomain, session) {
42
+ await this.ensureConnected();
43
+
44
+ const key = `${this.prefix}${siteDomain}`;
45
+ const sessionData = JSON.stringify(session);
46
+
47
+ if (session.expires_at) {
48
+ const ttl = Math.floor((session.expires_at - Date.now()) / 1000);
49
+ if (ttl > 0) {
50
+ await this.client.setEx(key, ttl, sessionData);
51
+ } else {
52
+ await this.client.set(key, sessionData);
53
+ }
54
+ } else {
55
+ await this.client.set(key, sessionData);
56
+ }
57
+
58
+ return true;
59
+ }
60
+
61
+ async load(siteDomain) {
62
+ await this.ensureConnected();
63
+
64
+ const key = `${this.prefix}${siteDomain}`;
65
+ const data = await this.client.get(key);
66
+
67
+ if (!data) return null;
68
+
69
+ try {
70
+ return JSON.parse(data);
71
+ } catch (error) {
72
+ console.error('Failed to parse Redis session data:', error);
73
+ return null;
74
+ }
75
+ }
76
+
77
+ async delete(siteDomain) {
78
+ await this.ensureConnected();
79
+
80
+ const key = `${this.prefix}${siteDomain}`;
81
+ const result = await this.client.del(key);
82
+ return result > 0;
83
+ }
84
+
85
+ async list() {
86
+ await this.ensureConnected();
87
+
88
+ const keys = await this.client.keys(`${this.prefix}*`);
89
+ const sessions = [];
90
+
91
+ for (const key of keys) {
92
+ const data = await this.client.get(key);
93
+ if (data) {
94
+ try {
95
+ sessions.push(JSON.parse(data));
96
+ } catch (error) {
97
+ console.error(`Failed to parse session from key ${key}:`, error);
98
+ }
99
+ }
100
+ }
101
+
102
+ return sessions;
103
+ }
104
+
105
+ async clear() {
106
+ await this.ensureConnected();
107
+
108
+ const keys = await this.client.keys(`${this.prefix}*`);
109
+ if (keys.length > 0) {
110
+ await this.client.del(keys);
111
+ }
112
+ return true;
113
+ }
114
+
115
+ async close() {
116
+ if (this.client) {
117
+ await this.client.quit();
118
+ }
119
+ return true;
120
+ }
121
+
122
+ async ping() {
123
+ await this.ensureConnected();
124
+ return await this.client.ping();
125
+ }
126
+
127
+ async info() {
128
+ await this.ensureConnected();
129
+ return await this.client.info();
130
+ }
131
+ }
132
+
133
+ module.exports = RedisStorage;
@@ -0,0 +1,133 @@
1
+ // src/Auth/session/storage/SQLiteStorage.js
2
+ const sqlite3 = require('sqlite3').verbose();
3
+
4
+ class SQLiteStorage {
5
+ constructor(options = {}) {
6
+ this.dbPath = options.path || './joonweb-sessions.db';
7
+ this.db = new sqlite3.Database(this.dbPath);
8
+ this.initDatabase();
9
+ }
10
+
11
+ async initDatabase() {
12
+ return new Promise((resolve, reject) => {
13
+ this.db.run(`
14
+ CREATE TABLE IF NOT EXISTS sessions (
15
+ site_domain TEXT PRIMARY KEY,
16
+ access_token TEXT NOT NULL,
17
+ scope TEXT,
18
+ user_data TEXT,
19
+ expires_at INTEGER,
20
+ authenticated_at INTEGER,
21
+ created_at INTEGER DEFAULT (strftime('%s', 'now')),
22
+ updated_at INTEGER DEFAULT (strftime('%s', 'now')),
23
+ metadata TEXT
24
+ )
25
+ `, (err) => {
26
+ if (err) reject(err);
27
+ else resolve();
28
+ });
29
+ });
30
+ }
31
+
32
+ async store(siteDomain, session) {
33
+ return new Promise((resolve, reject) => {
34
+ this.db.run(
35
+ `INSERT OR REPLACE INTO sessions
36
+ (site_domain, access_token, scope, user_data, expires_at,
37
+ authenticated_at, updated_at, metadata)
38
+ VALUES (?, ?, ?, ?, ?, ?, strftime('%s', 'now'), ?)`,
39
+ [
40
+ siteDomain,
41
+ session.access_token,
42
+ session.scope,
43
+ JSON.stringify(session.user || {}),
44
+ session.expires_at,
45
+ session.authenticated_at,
46
+ JSON.stringify(session.metadata || {})
47
+ ],
48
+ (err) => {
49
+ if (err) reject(err);
50
+ else resolve(true);
51
+ }
52
+ );
53
+ });
54
+ }
55
+
56
+ async load(siteDomain) {
57
+ return new Promise((resolve, reject) => {
58
+ this.db.get(
59
+ 'SELECT * FROM sessions WHERE site_domain = ?',
60
+ [siteDomain],
61
+ (err, row) => {
62
+ if (err) {
63
+ reject(err);
64
+ } else if (row) {
65
+ // Parse JSON fields
66
+ const session = {
67
+ site_domain: row.site_domain,
68
+ access_token: row.access_token,
69
+ scope: row.scope,
70
+ user: JSON.parse(row.user_data || '{}'),
71
+ expires_at: row.expires_at,
72
+ authenticated_at: row.authenticated_at,
73
+ metadata: JSON.parse(row.metadata || {})
74
+ };
75
+ resolve(session);
76
+ } else {
77
+ resolve(null);
78
+ }
79
+ }
80
+ );
81
+ });
82
+ }
83
+
84
+ async delete(siteDomain) {
85
+ return new Promise((resolve, reject) => {
86
+ this.db.run('DELETE FROM sessions WHERE site_domain = ?', [siteDomain], (err) => {
87
+ if (err) reject(err);
88
+ else resolve(true);
89
+ });
90
+ });
91
+ }
92
+
93
+ async list() {
94
+ return new Promise((resolve, reject) => {
95
+ this.db.all('SELECT * FROM sessions', (err, rows) => {
96
+ if (err) {
97
+ reject(err);
98
+ } else {
99
+ const sessions = rows.map(row => ({
100
+ site_domain: row.site_domain,
101
+ access_token: row.access_token,
102
+ scope: row.scope,
103
+ user: JSON.parse(row.user_data || '{}'),
104
+ expires_at: row.expires_at,
105
+ authenticated_at: row.authenticated_at,
106
+ metadata: JSON.parse(row.metadata || {})
107
+ }));
108
+ resolve(sessions);
109
+ }
110
+ });
111
+ });
112
+ }
113
+
114
+ async clear() {
115
+ return new Promise((resolve, reject) => {
116
+ this.db.run('DELETE FROM sessions', (err) => {
117
+ if (err) reject(err);
118
+ else resolve(true);
119
+ });
120
+ });
121
+ }
122
+
123
+ async close() {
124
+ return new Promise((resolve, reject) => {
125
+ this.db.close((err) => {
126
+ if (err) reject(err);
127
+ else resolve(true);
128
+ });
129
+ });
130
+ }
131
+ }
132
+
133
+ module.exports = SQLiteStorage;
@@ -0,0 +1,46 @@
1
+ // src/Auth/session/storage/index.js
2
+ const MemoryStorage = require('./MemoryStorage');
3
+ const SQLiteStorage = require('./SQLiteStorage');
4
+ const MongoDBStorage = require('./MongoDBStorage');
5
+ const RedisStorage = require('./RedisStorage');
6
+ const MySQLStorage = require('./MySQLStorage');
7
+ const PostgreSQLStorage = require('./PostgreSQLStorage');
8
+
9
+ function createStorageAdapter(type = 'memory', options = {}) {
10
+ const normalizedType = type.toLowerCase();
11
+
12
+ switch (normalizedType) {
13
+ case 'sqlite':
14
+ return new SQLiteStorage(options);
15
+
16
+ case 'mongodb':
17
+ return new MongoDBStorage(options);
18
+
19
+ case 'redis':
20
+ return new RedisStorage(options);
21
+
22
+ case 'mysql':
23
+ case 'mariadb':
24
+ return new MySQLStorage(options);
25
+
26
+ case 'postgres':
27
+ case 'postgresql':
28
+ case 'pgsql':
29
+ return new PostgreSQLStorage(options);
30
+
31
+ case 'memory':
32
+ default:
33
+ return new MemoryStorage();
34
+ }
35
+ }
36
+
37
+ // Export everything
38
+ module.exports = {
39
+ MemoryStorage,
40
+ SQLiteStorage,
41
+ MongoDBStorage,
42
+ RedisStorage,
43
+ MySQLStorage,
44
+ PostgreSQLStorage,
45
+ createStorageAdapter
46
+ };