Skip to content

Commit 12126da

Browse files
feat: Add automatic login and core API with Redis integration
1 parent 094254b commit 12126da

2 files changed

Lines changed: 209 additions & 7 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,5 @@
8585
- Hardcoded URLs with embedded credentials
8686
- # Never use emojis and speak in plain developer english for comments not AI comments
8787
- Ask 1-3 questions for clarity for any command given that is complex, go question by question
88-
- Ask questions one at a time before moving on allow user to skip
88+
- Ask questions one at a time before moving on allow user to skip
89+
- a

src/servers/railway/storage-test.ts

Lines changed: 207 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,138 @@ async function startServer() {
5959
// Root endpoint
6060
app.get('/', (req, res) => {
6161
res.json({
62-
name: 'StackMemory Storage Test Server',
63-
version: '1.0.0',
62+
name: 'StackMemory Core API',
63+
version: '0.3.17',
64+
description: 'Core API with Redis, PostgreSQL, and Railway Buckets',
6465
endpoints: [
6566
'/health',
66-
'/api/health',
67+
'/api/health',
68+
'/api/login',
69+
'/api/status',
6770
'/test-storage',
6871
'/create-frame',
69-
'/list-frames'
70-
]
72+
'/list-frames',
73+
'/api/frames'
74+
],
75+
storage_tiers: {
76+
hot: 'Redis (< 24 hours)',
77+
warm: 'Railway Buckets (1-30 days)',
78+
cold: 'PostgreSQL (30+ days)'
79+
}
7180
});
7281
});
7382

83+
// Auto-login endpoint for seamless API access
84+
app.post('/api/login', async (req, res) => {
85+
try {
86+
const { username, api_key } = req.body;
87+
88+
// Simple API key validation (in production, use proper JWT/OAuth)
89+
const validApiKey = process.env.API_KEY_SECRET || 'development-secret';
90+
91+
if (api_key === validApiKey) {
92+
// Store session in Redis if available
93+
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
94+
const sessionData = {
95+
username: username || 'api_user',
96+
created_at: new Date().toISOString(),
97+
api_key_hash: 'valid',
98+
permissions: ['read', 'write', 'admin']
99+
};
100+
101+
if (config.redisUrl) {
102+
const redisClient = createClient({ url: config.redisUrl });
103+
await redisClient.connect();
104+
await redisClient.setEx(`session:${sessionId}`, 3600, JSON.stringify(sessionData)); // 1 hour
105+
await redisClient.disconnect();
106+
}
107+
108+
res.json({
109+
success: true,
110+
session_id: sessionId,
111+
message: 'Automatically logged into StackMemory Core API',
112+
access: {
113+
redis: config.redisUrl ? 'available' : 'not_configured',
114+
postgresql: config.databaseUrl ? 'available' : 'not_configured',
115+
buckets: 'configurable'
116+
}
117+
});
118+
} else {
119+
res.status(401).json({
120+
success: false,
121+
message: 'Invalid API key'
122+
});
123+
}
124+
} catch (error: any) {
125+
res.status(500).json({
126+
success: false,
127+
message: 'Login failed',
128+
error: error.message
129+
});
130+
}
131+
});
132+
133+
// Core API status endpoint
134+
app.get('/api/status', async (req, res) => {
135+
const status: any = {
136+
service: 'stackmemory-core-api',
137+
version: '0.3.17',
138+
environment: process.env.NODE_ENV || 'production',
139+
timestamp: new Date().toISOString(),
140+
storage: {
141+
tiers: {}
142+
}
143+
};
144+
145+
// Check Redis (Hot Tier)
146+
if (config.redisUrl) {
147+
try {
148+
const redisClient = createClient({ url: config.redisUrl });
149+
await redisClient.connect();
150+
151+
const info = await redisClient.info('memory');
152+
const keys = await redisClient.keys('*');
153+
const usedMemory = info.match(/used_memory_human:(.+)/)?.[1];
154+
155+
status.storage.tiers.hot_redis = {
156+
status: 'connected',
157+
memory_used: usedMemory?.trim(),
158+
keys: keys.length,
159+
ttl_policy: 'auto-expire'
160+
};
161+
162+
await redisClient.disconnect();
163+
} catch (error: any) {
164+
status.storage.tiers.hot_redis = { status: 'error', message: error.message };
165+
}
166+
} else {
167+
status.storage.tiers.hot_redis = { status: 'not_configured' };
168+
}
169+
170+
// Check PostgreSQL (Cold Tier)
171+
if (config.databaseUrl) {
172+
try {
173+
const pgClient = new Client({ connectionString: config.databaseUrl });
174+
await pgClient.connect();
175+
176+
const result = await pgClient.query('SELECT COUNT(*) as frames FROM frames WHERE created_at > NOW() - INTERVAL \'24 hours\'');
177+
const totalFrames = await pgClient.query('SELECT COUNT(*) as total FROM frames');
178+
179+
status.storage.tiers.cold_postgresql = {
180+
status: 'connected',
181+
recent_frames: parseInt(result.rows[0].frames),
182+
total_frames: parseInt(totalFrames.rows[0].total)
183+
};
184+
185+
await pgClient.end();
186+
} catch (error: any) {
187+
status.storage.tiers.cold_postgresql = { status: 'error', message: error.message };
188+
}
189+
}
190+
191+
res.json(status);
192+
});
193+
74194
// Comprehensive storage test endpoint
75195
app.get('/test-storage', async (req, res) => {
76196
const results: StorageTestResult = {
@@ -316,7 +436,88 @@ async function startServer() {
316436
}
317437
});
318438

319-
// List recent frames
439+
// Core API frames endpoint
440+
app.get('/api/frames', async (req, res) => {
441+
try {
442+
const limit = parseInt(req.query.limit as string) || 10;
443+
const source = req.query.source as string || 'all'; // 'redis', 'postgresql', 'all'
444+
445+
const results: any = {
446+
frames: [],
447+
sources_checked: [],
448+
total_count: 0
449+
};
450+
451+
// From Redis (hot tier) - most recent
452+
if ((source === 'all' || source === 'redis') && config.redisUrl) {
453+
try {
454+
const redisClient = createClient({ url: config.redisUrl });
455+
await redisClient.connect();
456+
const redisKeys = await redisClient.keys('frame:*');
457+
const redisFrames = [];
458+
for (const key of redisKeys.slice(0, limit)) {
459+
const data = await redisClient.get(key);
460+
if (data) {
461+
const frame = JSON.parse(data);
462+
frame.source = 'redis';
463+
frame.tier = 'hot';
464+
redisFrames.push(frame);
465+
}
466+
}
467+
await redisClient.disconnect();
468+
results.frames.push(...redisFrames);
469+
results.sources_checked.push('redis');
470+
} catch (error: any) {
471+
results.redis_error = error.message;
472+
}
473+
}
474+
475+
// From PostgreSQL (cold tier) - persistent storage
476+
if ((source === 'all' || source === 'postgresql') && config.databaseUrl) {
477+
try {
478+
const pgClient = new Client({ connectionString: config.databaseUrl });
479+
await pgClient.connect();
480+
const pgFrames = await pgClient.query(
481+
'SELECT *, \'postgresql\' as source, \'cold\' as tier FROM frames ORDER BY created_at DESC LIMIT $1',
482+
[limit]
483+
);
484+
await pgClient.end();
485+
results.frames.push(...pgFrames.rows);
486+
results.sources_checked.push('postgresql');
487+
results.total_count = pgFrames.rowCount;
488+
} catch (error: any) {
489+
results.postgresql_error = error.message;
490+
}
491+
}
492+
493+
// Sort by created_at if multiple sources
494+
if (results.frames.length > 1) {
495+
results.frames.sort((a: any, b: any) =>
496+
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
497+
);
498+
}
499+
500+
res.json({
501+
count: results.frames.length,
502+
frames: results.frames.slice(0, limit),
503+
metadata: {
504+
sources_checked: results.sources_checked,
505+
total_in_postgresql: results.total_count,
506+
tier_explanation: {
507+
hot: 'Redis - Recent frames (< 24 hours)',
508+
cold: 'PostgreSQL - All frames (persistent storage)'
509+
}
510+
}
511+
});
512+
} catch (error: any) {
513+
res.status(500).json({
514+
error: 'Failed to fetch frames',
515+
message: error.message
516+
});
517+
}
518+
});
519+
520+
// List recent frames (legacy endpoint)
320521
app.get('/list-frames', async (req, res) => {
321522
const results: any = { sources: {} };
322523

0 commit comments

Comments
 (0)