redesign fully scaffolded and web login works

This commit is contained in:
2026-03-17 20:10:47 -04:00
parent b9cc397e05
commit f6bd22a8ef
143 changed files with 17317 additions and 693 deletions

View 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();
}
}
}

View 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>>;