redesign fully scaffolded and web login works
This commit is contained in:
173
gateway/src/auth/auth-service.ts
Normal file
173
gateway/src/auth/auth-service.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type { BetterAuthInstance } from './better-auth-config.js';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
import type { Pool } from 'pg';
|
||||
|
||||
export interface AuthServiceConfig {
|
||||
auth: BetterAuthInstance;
|
||||
pool: Pool;
|
||||
logger: FastifyBaseLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication service that integrates Better Auth with existing user system
|
||||
*/
|
||||
export class AuthService {
|
||||
private config: AuthServiceConfig;
|
||||
|
||||
constructor(config: AuthServiceConfig) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify JWT token and return user ID
|
||||
* Replaces the placeholder implementation in UserService
|
||||
*/
|
||||
async verifyToken(token: string): Promise<string | null> {
|
||||
try {
|
||||
// Better Auth's session verification
|
||||
const session = await this.config.auth.api.getSession({
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!session || !session.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return session.user.id;
|
||||
} catch (error) {
|
||||
this.config.logger.debug({ error }, 'Token verification failed');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create user with email and password
|
||||
*/
|
||||
async createUser(email: string, password: string, name?: string): Promise<{ userId: string; error?: string }> {
|
||||
try {
|
||||
const result = await this.config.auth.api.signUpEmail({
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
name: name || email.split('@')[0],
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.user) {
|
||||
return {
|
||||
userId: '',
|
||||
error: 'Failed to create user',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
userId: result.user.id,
|
||||
};
|
||||
} catch (error: any) {
|
||||
this.config.logger.error({ error }, 'User creation failed');
|
||||
return {
|
||||
userId: '',
|
||||
error: error.message || 'User creation failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign in with email and password
|
||||
*/
|
||||
async signIn(email: string, password: string): Promise<{ token: string; userId: string; error?: string }> {
|
||||
try {
|
||||
const result = await this.config.auth.api.signInEmail({
|
||||
body: {
|
||||
email,
|
||||
password,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result.token || !result.user) {
|
||||
return {
|
||||
token: '',
|
||||
userId: '',
|
||||
error: 'Invalid credentials',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
token: result.token,
|
||||
userId: result.user.id,
|
||||
};
|
||||
} catch (error: any) {
|
||||
this.config.logger.error({ error }, 'Sign in failed');
|
||||
return {
|
||||
token: '',
|
||||
userId: '',
|
||||
error: error.message || 'Sign in failed',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign out and invalidate session
|
||||
*/
|
||||
async signOut(token: string): Promise<{ success: boolean }> {
|
||||
try {
|
||||
await this.config.auth.api.signOut({
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
this.config.logger.error({ error }, 'Sign out failed');
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session from token
|
||||
*/
|
||||
async getSession(token: string) {
|
||||
try {
|
||||
const session = await this.config.auth.api.getSession({
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
return session;
|
||||
} catch (error) {
|
||||
this.config.logger.debug({ error }, 'Get session failed');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure user has a license (create default license if needed)
|
||||
*/
|
||||
async ensureUserLicense(userId: string, email: string): Promise<void> {
|
||||
const client = await this.config.pool.connect();
|
||||
try {
|
||||
// Check if license exists
|
||||
const licenseCheck = await client.query(
|
||||
'SELECT user_id FROM user_licenses WHERE user_id = $1',
|
||||
[userId]
|
||||
);
|
||||
|
||||
if (licenseCheck.rows.length === 0) {
|
||||
// Create default free license
|
||||
await client.query(
|
||||
`INSERT INTO user_licenses (user_id, email, license_type, mcp_server_url)
|
||||
VALUES ($1, $2, 'free', 'pending')`,
|
||||
[userId, email]
|
||||
);
|
||||
|
||||
this.config.logger.info({ userId }, 'Created default free license for new user');
|
||||
}
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
106
gateway/src/auth/better-auth-config.ts
Normal file
106
gateway/src/auth/better-auth-config.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { betterAuth } from 'better-auth';
|
||||
import { Pool } from 'pg';
|
||||
import { Kysely, PostgresDialect } from 'kysely';
|
||||
import type { FastifyBaseLogger } from 'fastify';
|
||||
|
||||
export interface BetterAuthConfig {
|
||||
databaseUrl: string;
|
||||
pool?: Pool;
|
||||
secret: string;
|
||||
baseUrl: string;
|
||||
trustedOrigins: string[];
|
||||
logger: FastifyBaseLogger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Better Auth instance with PostgreSQL adapter and passkey support
|
||||
*/
|
||||
export async function createBetterAuth(config: BetterAuthConfig) {
|
||||
try {
|
||||
config.logger.debug({
|
||||
databaseUrl: config.databaseUrl.replace(/:[^:@]+@/, ':***@'),
|
||||
baseUrl: config.baseUrl,
|
||||
}, 'Creating Better Auth instance');
|
||||
|
||||
// Use existing pool if provided, otherwise create new one
|
||||
const pool = config.pool || new Pool({
|
||||
connectionString: config.databaseUrl,
|
||||
});
|
||||
|
||||
config.logger.debug('PostgreSQL pool created');
|
||||
|
||||
// Test database connection first
|
||||
try {
|
||||
config.logger.debug('Testing database connection...');
|
||||
const testClient = await pool.connect();
|
||||
await testClient.query('SELECT 1');
|
||||
testClient.release();
|
||||
config.logger.debug('Database connection test successful');
|
||||
} catch (dbError: any) {
|
||||
config.logger.error({
|
||||
error: dbError,
|
||||
message: dbError.message,
|
||||
stack: dbError.stack,
|
||||
}, 'Database connection test failed');
|
||||
throw new Error(`Database connection failed: ${dbError.message}`);
|
||||
}
|
||||
|
||||
// Create Kysely instance for Better Auth
|
||||
config.logger.debug('Creating Kysely database instance...');
|
||||
const db = new Kysely({
|
||||
dialect: new PostgresDialect({ pool }),
|
||||
});
|
||||
config.logger.debug('Kysely instance created');
|
||||
|
||||
// Better Auth v1.5.3 postgres configuration
|
||||
const auth = betterAuth({
|
||||
database: {
|
||||
db,
|
||||
type: 'postgres',
|
||||
},
|
||||
|
||||
// Secret for JWT signing
|
||||
secret: config.secret,
|
||||
|
||||
// Base URL for callbacks and redirects
|
||||
baseURL: config.baseUrl,
|
||||
|
||||
// Trusted origins for CORS
|
||||
trustedOrigins: config.trustedOrigins,
|
||||
|
||||
// Email/password authentication
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
requireEmailVerification: false, // Set to true in production
|
||||
sendResetPassword: async ({ user, url }) => {
|
||||
// TODO: Implement email sending
|
||||
config.logger.info({ userId: user.id, resetUrl: url }, 'Password reset requested');
|
||||
},
|
||||
},
|
||||
|
||||
// Session configuration
|
||||
session: {
|
||||
expiresIn: 60 * 60 * 24 * 7, // 7 days
|
||||
updateAge: 60 * 60 * 24, // Update session every 24 hours
|
||||
cookieCache: {
|
||||
enabled: true,
|
||||
maxAge: 5 * 60, // 5 minutes
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
config.logger.debug('Better Auth instance created');
|
||||
return auth;
|
||||
} catch (error: any) {
|
||||
config.logger.error({
|
||||
error,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
cause: error.cause,
|
||||
}, 'Error creating Better Auth instance');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export type BetterAuthInstance = Awaited<ReturnType<typeof createBetterAuth>>;
|
||||
Reference in New Issue
Block a user