Skip to content

Commit 2bb23b5

Browse files
feat: Complete database schema support for authentication
- Added password_hash column to users table in both PostgreSQL and SQLite - Added migrations (v4 for PG, v3 for SQLite) to add password_hash column - Made email field UNIQUE in users table for both databases - Updated CLI version from 0.3.17 to 0.3.20 to match package.json - Database now fully supports signup and login authentication flow This completes the authentication infrastructure requested for the Railway API.
1 parent 83ff993 commit 2bb23b5

3 files changed

Lines changed: 224 additions & 4 deletions

File tree

src/cli/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import Database from 'better-sqlite3';
4747
import { join } from 'path';
4848
import { existsSync, mkdirSync } from 'fs';
4949

50-
const VERSION = '0.3.17';
50+
const VERSION = '0.3.20';
5151

5252
// Check for updates on CLI startup
5353
UpdateChecker.checkForUpdates(VERSION, true).catch(() => {

src/servers/railway/index.ts

Lines changed: 223 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,15 +126,17 @@ class RailwayMCPServer {
126126
await this.pgPool.query(`
127127
CREATE TABLE IF NOT EXISTS users (
128128
id TEXT PRIMARY KEY,
129-
email TEXT,
129+
email TEXT UNIQUE,
130130
name TEXT,
131+
password_hash TEXT,
131132
tier TEXT DEFAULT 'free',
132133
role TEXT DEFAULT 'user',
133134
created_at TIMESTAMPTZ DEFAULT NOW(),
134135
updated_at TIMESTAMPTZ DEFAULT NOW()
135136
);
136137
`);
137138
try { await this.pgPool.query(`ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'`); } catch {}
139+
try { await this.pgPool.query(`ALTER TABLE users ADD COLUMN password_hash TEXT`); } catch {}
138140
// Role constraints (best-effort)
139141
try { await this.pgPool.query(`ALTER TABLE project_members ADD CONSTRAINT project_members_role_check CHECK (role IN ('admin','owner','editor','viewer'))`); } catch {}
140142
try { await this.pgPool.query(`ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('admin','user'))`); } catch {}
@@ -211,8 +213,9 @@ class RailwayMCPServer {
211213
212214
CREATE TABLE IF NOT EXISTS users (
213215
id TEXT PRIMARY KEY,
214-
email TEXT,
216+
email TEXT UNIQUE,
215217
name TEXT,
218+
password_hash TEXT,
216219
tier TEXT DEFAULT 'free',
217220
role TEXT DEFAULT 'user',
218221
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -306,6 +309,12 @@ class RailwayMCPServer {
306309
`ALTER TABLE project_members ADD CONSTRAINT project_members_role_check CHECK (role IN ('admin','owner','editor','viewer'))`,
307310
`ALTER TABLE users ADD CONSTRAINT users_role_check CHECK (role IN ('admin','user'))`
308311
]);
312+
313+
// v4: password authentication support
314+
await apply(4, 'password authentication', [
315+
`ALTER TABLE users ADD COLUMN IF NOT EXISTS password_hash TEXT`,
316+
`ALTER TABLE users ADD CONSTRAINT users_email_unique UNIQUE (email)`
317+
]);
309318
} else {
310319
// sqlite
311320
this.db.exec(`CREATE TABLE IF NOT EXISTS railway_schema_version (version INTEGER PRIMARY KEY, applied_at DATETIME DEFAULT CURRENT_TIMESTAMP, description TEXT)`);
@@ -342,6 +351,11 @@ class RailwayMCPServer {
342351
`CREATE TABLE IF NOT EXISTS admin_sessions (id TEXT PRIMARY KEY, user_id TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME NOT NULL, user_agent TEXT, ip TEXT)`,
343352
`CREATE INDEX IF NOT EXISTS idx_admin_sessions_user ON admin_sessions(user_id)`
344353
]);
354+
355+
// v3: password authentication support
356+
apply(3, 'password authentication', [
357+
`ALTER TABLE users ADD COLUMN password_hash TEXT`
358+
]);
345359
}
346360
}
347361

@@ -535,6 +549,213 @@ class RailwayMCPServer {
535549
});
536550
});
537551

552+
// Authentication Routes
553+
this.app.post('/auth/signup', async (req, res) => {
554+
try {
555+
const { email, password, name } = req.body;
556+
557+
// Validate input
558+
if (!email || !password) {
559+
return res.status(400).json({
560+
success: false,
561+
error: 'Email and password are required'
562+
});
563+
}
564+
565+
// Check if user already exists
566+
if (this.pgPool) {
567+
const existingUser = await this.pgPool.query(
568+
'SELECT id FROM users WHERE email = $1',
569+
[email]
570+
);
571+
if (existingUser.rowCount > 0) {
572+
return res.status(409).json({
573+
success: false,
574+
error: 'User already exists'
575+
});
576+
}
577+
} else {
578+
const existingUser = this.db.prepare('SELECT id FROM users WHERE email = ?').get(email);
579+
if (existingUser) {
580+
return res.status(409).json({
581+
success: false,
582+
error: 'User already exists'
583+
});
584+
}
585+
}
586+
587+
// Hash password
588+
const passwordHash = await bcrypt.hash(password, 10);
589+
const userId = `user_${Date.now()}_${Math.random().toString(36).substring(7)}`;
590+
591+
// Create user
592+
if (this.pgPool) {
593+
await this.pgPool.query(
594+
'INSERT INTO users (id, email, name, password_hash, tier, role) VALUES ($1, $2, $3, $4, $5, $6)',
595+
[userId, email, name || email.split('@')[0], passwordHash, 'free', 'user']
596+
);
597+
} else {
598+
this.db.prepare(
599+
'INSERT INTO users (id, email, name, password_hash, tier, role) VALUES (?, ?, ?, ?, ?, ?)'
600+
).run(userId, email, name || email.split('@')[0], passwordHash, 'free', 'user');
601+
}
602+
603+
// Generate API key for the user
604+
const apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
605+
const apiKeyHash = await bcrypt.hash(apiKey, 10);
606+
607+
if (this.pgPool) {
608+
await this.pgPool.query(
609+
'INSERT INTO api_keys (key_hash, user_id, name) VALUES ($1, $2, $3)',
610+
[apiKeyHash, userId, 'Default API Key']
611+
);
612+
} else {
613+
this.db.prepare(
614+
'INSERT INTO api_keys (key_hash, user_id, name) VALUES (?, ?, ?)'
615+
).run(apiKeyHash, userId, 'Default API Key');
616+
}
617+
618+
// Generate JWT token
619+
const token = jwt.sign(
620+
{ sub: userId, email, role: 'user' },
621+
config.jwtSecret,
622+
{ expiresIn: '30d' }
623+
);
624+
625+
res.json({
626+
success: true,
627+
apiKey,
628+
token,
629+
email,
630+
userId,
631+
message: 'Account created successfully'
632+
});
633+
} catch (error: any) {
634+
console.error('Signup error:', error);
635+
res.status(500).json({
636+
success: false,
637+
error: 'Failed to create account'
638+
});
639+
}
640+
});
641+
642+
this.app.post('/auth/login', async (req, res) => {
643+
try {
644+
const { email, password } = req.body;
645+
646+
// Validate input
647+
if (!email || !password) {
648+
return res.status(400).json({
649+
success: false,
650+
error: 'Email and password are required'
651+
});
652+
}
653+
654+
// Find user
655+
let user: any = null;
656+
if (this.pgPool) {
657+
const result = await this.pgPool.query(
658+
'SELECT id, email, name, password_hash, tier, role FROM users WHERE email = $1',
659+
[email]
660+
);
661+
user = result.rows[0];
662+
} else {
663+
user = this.db.prepare(
664+
'SELECT id, email, name, password_hash, tier, role FROM users WHERE email = ?'
665+
).get(email);
666+
}
667+
668+
if (!user) {
669+
return res.status(401).json({
670+
success: false,
671+
error: 'Invalid credentials'
672+
});
673+
}
674+
675+
// Verify password
676+
const validPassword = await bcrypt.compare(password, user.password_hash);
677+
if (!validPassword) {
678+
return res.status(401).json({
679+
success: false,
680+
error: 'Invalid credentials'
681+
});
682+
}
683+
684+
// Get or create API key
685+
let apiKey: string | null = null;
686+
if (this.pgPool) {
687+
// Try to get existing API key
688+
const keyResult = await this.pgPool.query(
689+
'SELECT id FROM api_keys WHERE user_id = $1 AND revoked = false LIMIT 1',
690+
[user.id]
691+
);
692+
693+
if (keyResult.rowCount === 0) {
694+
// Create new API key
695+
apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
696+
const apiKeyHash = await bcrypt.hash(apiKey, 10);
697+
await this.pgPool.query(
698+
'INSERT INTO api_keys (key_hash, user_id, name) VALUES ($1, $2, $3)',
699+
[apiKeyHash, user.id, 'Default API Key']
700+
);
701+
} else {
702+
// For existing keys, we can't retrieve the original, so generate a new one
703+
apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
704+
const apiKeyHash = await bcrypt.hash(apiKey, 10);
705+
await this.pgPool.query(
706+
'UPDATE api_keys SET key_hash = $1, last_used = NOW() WHERE id = $2',
707+
[apiKeyHash, keyResult.rows[0].id]
708+
);
709+
}
710+
711+
// Get database URL for this user's context
712+
const databaseUrl = process.env.DATABASE_URL;
713+
} else {
714+
// SQLite version
715+
const keyRow = this.db.prepare(
716+
'SELECT id FROM api_keys WHERE user_id = ? AND revoked = 0 LIMIT 1'
717+
).get(user.id) as any;
718+
719+
if (!keyRow) {
720+
apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
721+
const apiKeyHash = await bcrypt.hash(apiKey, 10);
722+
this.db.prepare(
723+
'INSERT INTO api_keys (key_hash, user_id, name) VALUES (?, ?, ?)'
724+
).run(apiKeyHash, user.id, 'Default API Key');
725+
} else {
726+
apiKey = `sk_${Math.random().toString(36).substring(2)}${Math.random().toString(36).substring(2)}`;
727+
const apiKeyHash = await bcrypt.hash(apiKey, 10);
728+
this.db.prepare(
729+
'UPDATE api_keys SET key_hash = ?, last_used = CURRENT_TIMESTAMP WHERE id = ?'
730+
).run(apiKeyHash, keyRow.id);
731+
}
732+
}
733+
734+
// Generate JWT token
735+
const token = jwt.sign(
736+
{ sub: user.id, email: user.email, role: user.role },
737+
config.jwtSecret,
738+
{ expiresIn: '30d' }
739+
);
740+
741+
res.json({
742+
success: true,
743+
apiKey,
744+
token,
745+
email: user.email,
746+
userId: user.id,
747+
databaseUrl: process.env.DATABASE_URL, // For client configuration
748+
message: 'Login successful'
749+
});
750+
} catch (error: any) {
751+
console.error('Login error:', error);
752+
res.status(500).json({
753+
success: false,
754+
error: 'Login failed'
755+
});
756+
}
757+
});
758+
538759
// API Routes
539760
this.app.post('/api/context/save', async (req, res) => {
540761
try {

zeroshot

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)