backend changes
This commit is contained in:
parent
1c4f9b47ed
commit
a9964e906d
@ -6,11 +6,10 @@
|
|||||||
// ========================================
|
// ========================================
|
||||||
// LIVE PRODUCTION URLS (Currently Active)
|
// LIVE PRODUCTION URLS (Currently Active)
|
||||||
// ========================================
|
// ========================================
|
||||||
const FRONTEND_URL = 'https://dashboard.codenuk.com';
|
const FRONTEND_URL = 'http://192.168.1.31:3001';
|
||||||
const BACKEND_URL = 'https://backend.codenuk.com';
|
const BACKEND_URL = 'https://backend.codenuk.com';
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// LOCAL DEVELOPMENT URLS (Comment out live URLs above and uncomment these)
|
|
||||||
// ========================================
|
// ========================================
|
||||||
// const FRONTEND_URL = 'http://localhost:3001';
|
// const FRONTEND_URL = 'http://localhost:3001';
|
||||||
// const BACKEND_URL = 'http://localhost:8000';
|
// const BACKEND_URL = 'http://localhost:8000';
|
||||||
|
|||||||
@ -233,7 +233,7 @@ services:
|
|||||||
- NODE_ENV=development
|
- NODE_ENV=development
|
||||||
- PORT=8000
|
- PORT=8000
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- FRONTEND_URL=https://dashboard.codenuk.com # Allow all URLs
|
- FRONTEND_URL=http://192.168.1.31:3001 # Allow all URLs
|
||||||
- CORS_ORIGINS=* # Allow all URLs
|
- CORS_ORIGINS=* # Allow all URLs
|
||||||
- CORS_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS # Add this line
|
- CORS_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS # Add this line
|
||||||
- CORS_CREDENTIALS=true # Add this line
|
- CORS_CREDENTIALS=true # Add this line
|
||||||
@ -507,7 +507,7 @@ services:
|
|||||||
- JWT_ACCESS_EXPIRY=24h
|
- JWT_ACCESS_EXPIRY=24h
|
||||||
- JWT_ADMIN_ACCESS_EXPIRY=7d
|
- JWT_ADMIN_ACCESS_EXPIRY=7d
|
||||||
- JWT_REFRESH_EXPIRY=7d
|
- JWT_REFRESH_EXPIRY=7d
|
||||||
- FRONTEND_URL=https://dashboard.codenuk.com
|
- FRONTEND_URL=http://192.168.1.31:3001
|
||||||
# Email Configuration
|
# Email Configuration
|
||||||
- SMTP_HOST=smtp.gmail.com
|
- SMTP_HOST=smtp.gmail.com
|
||||||
- SMTP_PORT=587
|
- SMTP_PORT=587
|
||||||
@ -613,7 +613,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- PORT=8012
|
- PORT=8012
|
||||||
- HOST=0.0.0.0
|
- HOST=0.0.0.0
|
||||||
- FRONTEND_URL=https://dashboard.codenuk.com
|
- FRONTEND_URL=http://192.168.1.31:3001
|
||||||
- POSTGRES_HOST=postgres
|
- POSTGRES_HOST=postgres
|
||||||
- POSTGRES_PORT=5432
|
- POSTGRES_PORT=5432
|
||||||
- POSTGRES_DB=dev_pipeline
|
- POSTGRES_DB=dev_pipeline
|
||||||
|
|||||||
@ -28,9 +28,9 @@ RABBITMQ_USER=pipeline_admin
|
|||||||
RABBITMQ_PASSWORD=secure_rabbitmq_password
|
RABBITMQ_PASSWORD=secure_rabbitmq_password
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
FRONTEND_URL=https://dashboard.codenuk.com
|
FRONTEND_URL=http://192.168.1.31:3001
|
||||||
|
|
||||||
# CORS Configuration
|
# CORS Configuration
|
||||||
CORS_ORIGIN=https://dashboard.codenuk.com
|
CORS_ORIGIN=http://192.168.1.31:3001
|
||||||
CORS_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
|
CORS_METHODS=GET,POST,PUT,DELETE,PATCH,OPTIONS
|
||||||
CORS_CREDENTIALS=true
|
CORS_CREDENTIALS=true
|
||||||
@ -30,17 +30,18 @@ async function markMigrationApplied(version) {
|
|||||||
|
|
||||||
async function runMigrations() {
|
async function runMigrations() {
|
||||||
console.log('🚀 Starting template-manager database migrations...');
|
console.log('🚀 Starting template-manager database migrations...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create migrations tracking table first
|
// Create migrations tracking table first
|
||||||
await createMigrationsTable();
|
await createMigrationsTable();
|
||||||
console.log('✅ Migration tracking table ready');
|
console.log('✅ Migration tracking table ready');
|
||||||
|
|
||||||
// Get all migration files in order
|
// Get all migration files in order
|
||||||
|
// Reordered to ensure custom_templates table exists before admin_approval_workflow
|
||||||
const migrationFiles = [
|
const migrationFiles = [
|
||||||
'001_initial_schema.sql',
|
'001_initial_schema.sql',
|
||||||
'002_admin_approval_workflow.sql',
|
'003_custom_templates.sql', // Moved earlier since others depend on it
|
||||||
'003_custom_templates.sql',
|
'002_admin_approval_workflow.sql', // Now runs after custom_templates is created
|
||||||
'004_add_is_custom_flag.sql',
|
'004_add_is_custom_flag.sql',
|
||||||
'004_add_user_id_to_custom_templates.sql',
|
'004_add_user_id_to_custom_templates.sql',
|
||||||
'005_fix_custom_features_foreign_key.sql',
|
'005_fix_custom_features_foreign_key.sql',
|
||||||
@ -50,10 +51,10 @@ async function runMigrations() {
|
|||||||
|
|
||||||
let appliedCount = 0;
|
let appliedCount = 0;
|
||||||
let skippedCount = 0;
|
let skippedCount = 0;
|
||||||
|
|
||||||
for (const migrationFile of migrationFiles) {
|
for (const migrationFile of migrationFiles) {
|
||||||
const migrationPath = path.join(__dirname, migrationFile);
|
const migrationPath = path.join(__dirname, migrationFile);
|
||||||
|
|
||||||
// Check if migration file exists
|
// Check if migration file exists
|
||||||
if (!fs.existsSync(migrationPath)) {
|
if (!fs.existsSync(migrationPath)) {
|
||||||
console.log(`⚠️ Migration file not found: ${migrationFile}`);
|
console.log(`⚠️ Migration file not found: ${migrationFile}`);
|
||||||
@ -66,7 +67,7 @@ async function runMigrations() {
|
|||||||
skippedCount++;
|
skippedCount++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
|
const migrationSQL = fs.readFileSync(migrationPath, 'utf8');
|
||||||
|
|
||||||
// Skip destructive migrations unless explicitly allowed
|
// Skip destructive migrations unless explicitly allowed
|
||||||
@ -79,17 +80,17 @@ async function runMigrations() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📄 Running migration: ${migrationFile}`);
|
console.log(`📄 Running migration: ${migrationFile}`);
|
||||||
|
|
||||||
// Execute the migration
|
// Execute the migration
|
||||||
await database.query(migrationSQL);
|
await database.query(migrationSQL);
|
||||||
await markMigrationApplied(migrationFile);
|
await markMigrationApplied(migrationFile);
|
||||||
|
|
||||||
console.log(`✅ Migration ${migrationFile} completed!`);
|
console.log(`✅ Migration ${migrationFile} completed!`);
|
||||||
appliedCount++;
|
appliedCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📊 Migration summary: ${appliedCount} applied, ${skippedCount} skipped`);
|
console.log(`📊 Migration summary: ${appliedCount} applied, ${skippedCount} skipped`);
|
||||||
|
|
||||||
// Verify tables were created
|
// Verify tables were created
|
||||||
const result = await database.query(`
|
const result = await database.query(`
|
||||||
SELECT table_name
|
SELECT table_name
|
||||||
@ -98,9 +99,9 @@ async function runMigrations() {
|
|||||||
AND table_name IN ('templates', 'template_features', 'feature_business_rules', 'feature_usage', 'custom_features', 'custom_templates', 'feature_synonyms', 'admin_notifications')
|
AND table_name IN ('templates', 'template_features', 'feature_business_rules', 'feature_usage', 'custom_features', 'custom_templates', 'feature_synonyms', 'admin_notifications')
|
||||||
ORDER BY table_name
|
ORDER BY table_name
|
||||||
`);
|
`);
|
||||||
|
|
||||||
console.log('🔍 Verified tables:', result.rows.map(row => row.table_name));
|
console.log('🔍 Verified tables:', result.rows.map(row => row.table_name));
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Migration failed:', error.message);
|
console.error('❌ Migration failed:', error.message);
|
||||||
console.error('📍 Error details:', error);
|
console.error('📍 Error details:', error);
|
||||||
|
|||||||
@ -10,32 +10,32 @@ class AuthService {
|
|||||||
// Register new user
|
// Register new user
|
||||||
async register(userData) {
|
async register(userData) {
|
||||||
const { username, email, password, first_name, last_name } = userData;
|
const { username, email, password, first_name, last_name } = userData;
|
||||||
|
|
||||||
// Validate input
|
// Validate input
|
||||||
if (!username || !email || !password) {
|
if (!username || !email || !password) {
|
||||||
throw new Error('Username, email, and password are required');
|
throw new Error('Username, email, and password are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!User.validateEmail(email)) {
|
if (!User.validateEmail(email)) {
|
||||||
throw new Error('Invalid email format');
|
throw new Error('Invalid email format');
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordValidation = User.validatePassword(password);
|
const passwordValidation = User.validatePassword(password);
|
||||||
if (!passwordValidation.valid) {
|
if (!passwordValidation.valid) {
|
||||||
throw new Error(passwordValidation.message);
|
throw new Error(passwordValidation.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user already exists
|
// Check if user already exists
|
||||||
const existingUser = await User.findByEmail(email);
|
const existingUser = await User.findByEmail(email);
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
throw new Error('User with this email already exists');
|
throw new Error('User with this email already exists');
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUsername = await User.findByUsername(username);
|
const existingUsername = await User.findByUsername(username);
|
||||||
if (existingUsername) {
|
if (existingUsername) {
|
||||||
throw new Error('Username already taken');
|
throw new Error('Username already taken');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
const newUser = await User.create({
|
const newUser = await User.create({
|
||||||
username,
|
username,
|
||||||
@ -44,9 +44,9 @@ class AuthService {
|
|||||||
first_name,
|
first_name,
|
||||||
last_name
|
last_name
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`👤 New user registered: ${newUser.email}`);
|
console.log(`👤 New user registered: ${newUser.email}`);
|
||||||
|
|
||||||
// Send verification email (non-blocking but awaited to surface errors in dev)
|
// Send verification email (non-blocking but awaited to surface errors in dev)
|
||||||
try {
|
try {
|
||||||
await this.sendVerificationEmail(newUser);
|
await this.sendVerificationEmail(newUser);
|
||||||
@ -57,7 +57,7 @@ class AuthService {
|
|||||||
user: newUser.email,
|
user: newUser.email,
|
||||||
stack: err.stack
|
stack: err.stack
|
||||||
});
|
});
|
||||||
|
|
||||||
// In development, don't fail the registration if email fails
|
// In development, don't fail the registration if email fails
|
||||||
if (process.env.NODE_ENV === 'development') {
|
if (process.env.NODE_ENV === 'development') {
|
||||||
console.warn('⚠️ Registration completed but verification email failed. User can still login.');
|
console.warn('⚠️ Registration completed but verification email failed. User can still login.');
|
||||||
@ -66,7 +66,7 @@ class AuthService {
|
|||||||
console.error('🚨 Critical: Verification email failed in production environment');
|
console.error('🚨 Critical: Verification email failed in production environment');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newUser;
|
return newUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,46 +74,46 @@ class AuthService {
|
|||||||
async login(credentials, sessionInfo = {}) {
|
async login(credentials, sessionInfo = {}) {
|
||||||
const { email, password } = credentials;
|
const { email, password } = credentials;
|
||||||
const { ip_address, user_agent, device_info } = sessionInfo;
|
const { ip_address, user_agent, device_info } = sessionInfo;
|
||||||
|
|
||||||
if (!email || !password) {
|
if (!email || !password) {
|
||||||
throw new Error('Email and password are required');
|
throw new Error('Email and password are required');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find user
|
// Find user
|
||||||
const user = await User.findByEmail(email);
|
const user = await User.findByEmail(email);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('Invalid email or password');
|
throw new Error('Invalid email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Require email to be verified before allowing login
|
// Require email to be verified before allowing login
|
||||||
if (!user.email_verified) {
|
if (!user.email_verified) {
|
||||||
throw new Error('Please verify your email before logging in');
|
throw new Error('Please verify your email before logging in');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
const isPasswordValid = await user.verifyPassword(password);
|
const isPasswordValid = await user.verifyPassword(password);
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
throw new Error('Invalid email or password');
|
throw new Error('Invalid email or password');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last login
|
// Update last login
|
||||||
await user.updateLastLogin();
|
await user.updateLastLogin();
|
||||||
|
|
||||||
// Generate tokens
|
// Generate tokens
|
||||||
const tokens = jwtConfig.generateTokenPair(user);
|
const tokens = jwtConfig.generateTokenPair(user);
|
||||||
|
|
||||||
// Store refresh token
|
// Store refresh token
|
||||||
await this.storeRefreshToken(user.id, tokens.refreshToken);
|
await this.storeRefreshToken(user.id, tokens.refreshToken);
|
||||||
|
|
||||||
// Create session
|
// Create session
|
||||||
const session = await this.createSession(user.id, {
|
const session = await this.createSession(user.id, {
|
||||||
ip_address,
|
ip_address,
|
||||||
user_agent,
|
user_agent,
|
||||||
device_info
|
device_info
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`🔑 User logged in: ${user.email}`);
|
console.log(`🔑 User logged in: ${user.email}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: user.toJSON(),
|
user: user.toJSON(),
|
||||||
tokens,
|
tokens,
|
||||||
@ -148,9 +148,31 @@ class AuthService {
|
|||||||
|
|
||||||
async sendVerificationEmail(user) {
|
async sendVerificationEmail(user) {
|
||||||
const token = await this.createEmailVerificationToken(user.id);
|
const token = await this.createEmailVerificationToken(user.id);
|
||||||
// Send users to the frontend verification page; the frontend will call the backend and handle redirects
|
// Resolve verification URL. Prefer environment variable (works in Docker). If not present,
|
||||||
const { getVerificationUrl } = require('../../../../config/urls');
|
// fall back to the repository-level config/urls.js when available (development).
|
||||||
const verifyUrl = getVerificationUrl(token);
|
let verifyUrl;
|
||||||
|
const frontendUrlFromEnv = process.env.FRONTEND_URL;
|
||||||
|
if (frontendUrlFromEnv) {
|
||||||
|
const FRONTEND_URL = frontendUrlFromEnv.replace(/\/$/, '');
|
||||||
|
verifyUrl = `${FRONTEND_URL}/verify-email?token=${encodeURIComponent(token)}`;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
// Attempt to load repo-level config (works when running locally from repo root)
|
||||||
|
// This is guarded so it won't crash inside Docker if the relative path isn't valid.
|
||||||
|
// eslint-disable-next-line global-require
|
||||||
|
const urls = require('../../../../config/urls');
|
||||||
|
if (urls && typeof urls.getVerificationUrl === 'function') {
|
||||||
|
verifyUrl = urls.getVerificationUrl(token);
|
||||||
|
} else if (urls && urls.FRONTEND_URL) {
|
||||||
|
const FRONTEND_URL = urls.FRONTEND_URL.replace(/\/$/, '');
|
||||||
|
verifyUrl = `${FRONTEND_URL}/verify-email?token=${encodeURIComponent(token)}`;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// As a last resort, build a relative backend-hosted verification endpoint
|
||||||
|
const backendHost = process.env.BACKEND_URL || `http://localhost:${process.env.PORT || 8011}`;
|
||||||
|
verifyUrl = `${backendHost.replace(/\/$/, '')}/api/auth/verify-email?token=${encodeURIComponent(token)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const dateString = today.toLocaleDateString('en-US');
|
const dateString = today.toLocaleDateString('en-US');
|
||||||
@ -219,7 +241,7 @@ class AuthService {
|
|||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new Error('Refresh token is required');
|
throw new Error('Refresh token is required');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify refresh token
|
// Verify refresh token
|
||||||
let decoded;
|
let decoded;
|
||||||
try {
|
try {
|
||||||
@ -227,33 +249,33 @@ class AuthService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Invalid refresh token');
|
throw new Error('Invalid refresh token');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if token exists and is not revoked (support deterministic + legacy bcrypt storage)
|
// Check if token exists and is not revoked (support deterministic + legacy bcrypt storage)
|
||||||
const storedToken = await this.findStoredRefreshToken(decoded.userId, refreshToken);
|
const storedToken = await this.findStoredRefreshToken(decoded.userId, refreshToken);
|
||||||
|
|
||||||
if (!storedToken || storedToken.is_revoked) {
|
if (!storedToken || storedToken.is_revoked) {
|
||||||
throw new Error('Refresh token is revoked or invalid');
|
throw new Error('Refresh token is revoked or invalid');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (new Date() > storedToken.expires_at) {
|
if (new Date() > storedToken.expires_at) {
|
||||||
throw new Error('Refresh token has expired');
|
throw new Error('Refresh token has expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user
|
// Get user
|
||||||
const user = await User.findById(decoded.userId);
|
const user = await User.findById(decoded.userId);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new tokens
|
// Generate new tokens
|
||||||
const tokens = jwtConfig.generateTokenPair(user);
|
const tokens = jwtConfig.generateTokenPair(user);
|
||||||
|
|
||||||
// Revoke old refresh token and store new one
|
// Revoke old refresh token and store new one
|
||||||
await this.revokeRefreshTokenById(storedToken.id);
|
await this.revokeRefreshTokenById(storedToken.id);
|
||||||
await this.storeRefreshToken(user.id, tokens.refreshToken);
|
await this.storeRefreshToken(user.id, tokens.refreshToken);
|
||||||
|
|
||||||
console.log(`🔄 Token refreshed for user: ${user.email}`);
|
console.log(`🔄 Token refreshed for user: ${user.email}`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user: user.toJSON(),
|
user: user.toJSON(),
|
||||||
tokens
|
tokens
|
||||||
@ -273,11 +295,11 @@ class AuthService {
|
|||||||
console.warn('⚠️ Logout could not find refresh token to revoke:', e.message);
|
console.warn('⚠️ Logout could not find refresh token to revoke:', e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sessionToken) {
|
if (sessionToken) {
|
||||||
await this.endSession(sessionToken);
|
await this.endSession(sessionToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('🚪 User logged out');
|
console.log('🚪 User logged out');
|
||||||
return { message: 'Logged out successfully' };
|
return { message: 'Logged out successfully' };
|
||||||
}
|
}
|
||||||
@ -287,13 +309,13 @@ class AuthService {
|
|||||||
const tokenHash = this.hashDeterministic(refreshToken);
|
const tokenHash = this.hashDeterministic(refreshToken);
|
||||||
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at)
|
INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await database.query(query, [id, userId, tokenHash, expiresAt]);
|
const result = await database.query(query, [id, userId, tokenHash, expiresAt]);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
@ -304,7 +326,7 @@ class AuthService {
|
|||||||
SELECT * FROM refresh_tokens
|
SELECT * FROM refresh_tokens
|
||||||
WHERE token_hash = $1
|
WHERE token_hash = $1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await database.query(query, [tokenHash]);
|
const result = await database.query(query, [tokenHash]);
|
||||||
return result.rows[0] || null;
|
return result.rows[0] || null;
|
||||||
}
|
}
|
||||||
@ -316,7 +338,7 @@ class AuthService {
|
|||||||
SET is_revoked = true, revoked_at = NOW()
|
SET is_revoked = true, revoked_at = NOW()
|
||||||
WHERE token_hash = $1
|
WHERE token_hash = $1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await database.query(query, [tokenHash]);
|
await database.query(query, [tokenHash]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,14 +357,14 @@ class AuthService {
|
|||||||
const sessionToken = uuidv4();
|
const sessionToken = uuidv4();
|
||||||
const id = uuidv4();
|
const id = uuidv4();
|
||||||
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000); // 30 days
|
||||||
|
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO user_sessions (
|
INSERT INTO user_sessions (
|
||||||
id, user_id, session_token, ip_address, user_agent, device_info, expires_at
|
id, user_id, session_token, ip_address, user_agent, device_info, expires_at
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
id,
|
id,
|
||||||
userId,
|
userId,
|
||||||
@ -352,7 +374,7 @@ class AuthService {
|
|||||||
sessionInfo.device_info ? JSON.stringify(sessionInfo.device_info) : null,
|
sessionInfo.device_info ? JSON.stringify(sessionInfo.device_info) : null,
|
||||||
expiresAt
|
expiresAt
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = await database.query(query, values);
|
const result = await database.query(query, values);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
@ -364,7 +386,7 @@ class AuthService {
|
|||||||
SET is_active = false
|
SET is_active = false
|
||||||
WHERE session_token = $1
|
WHERE session_token = $1
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await database.query(query, [sessionToken]);
|
await database.query(query, [sessionToken]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -376,7 +398,7 @@ class AuthService {
|
|||||||
WHERE session_token = $1 AND is_active = true
|
WHERE session_token = $1 AND is_active = true
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await database.query(query, [sessionToken]);
|
const result = await database.query(query, [sessionToken]);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
@ -388,7 +410,7 @@ class AuthService {
|
|||||||
WHERE user_id = $1 AND is_active = true
|
WHERE user_id = $1 AND is_active = true
|
||||||
ORDER BY last_activity DESC
|
ORDER BY last_activity DESC
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await database.query(query, [userId]);
|
const result = await database.query(query, [userId]);
|
||||||
return result.rows;
|
return result.rows;
|
||||||
}
|
}
|
||||||
@ -398,11 +420,11 @@ class AuthService {
|
|||||||
try {
|
try {
|
||||||
const decoded = jwtConfig.verifyAccessToken(token);
|
const decoded = jwtConfig.verifyAccessToken(token);
|
||||||
const user = await User.findById(decoded.userId);
|
const user = await User.findById(decoded.userId);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Invalid access token');
|
throw new Error('Invalid access token');
|
||||||
@ -452,17 +474,17 @@ class AuthService {
|
|||||||
// Cleanup expired tokens and sessions
|
// Cleanup expired tokens and sessions
|
||||||
async cleanup() {
|
async cleanup() {
|
||||||
console.log('🧹 Starting auth cleanup...');
|
console.log('🧹 Starting auth cleanup...');
|
||||||
|
|
||||||
// Cleanup expired tokens
|
// Cleanup expired tokens
|
||||||
const tokenResult = await database.query('SELECT cleanup_expired_tokens()');
|
const tokenResult = await database.query('SELECT cleanup_expired_tokens()');
|
||||||
const deletedTokens = tokenResult.rows[0].cleanup_expired_tokens;
|
const deletedTokens = tokenResult.rows[0].cleanup_expired_tokens;
|
||||||
|
|
||||||
// Cleanup inactive sessions
|
// Cleanup inactive sessions
|
||||||
const sessionResult = await database.query('SELECT cleanup_inactive_sessions()');
|
const sessionResult = await database.query('SELECT cleanup_inactive_sessions()');
|
||||||
const inactiveSessions = sessionResult.rows[0].cleanup_inactive_sessions;
|
const inactiveSessions = sessionResult.rows[0].cleanup_inactive_sessions;
|
||||||
|
|
||||||
console.log(`🧹 Cleanup completed: ${deletedTokens} tokens, ${inactiveSessions} sessions`);
|
console.log(`🧹 Cleanup completed: ${deletedTokens} tokens, ${inactiveSessions} sessions`);
|
||||||
|
|
||||||
return { deletedTokens, inactiveSessions };
|
return { deletedTokens, inactiveSessions };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,17 +494,17 @@ class AuthService {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('User not found');
|
throw new Error('User not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordValidation = User.validatePassword(newPassword);
|
const passwordValidation = User.validatePassword(newPassword);
|
||||||
if (!passwordValidation.valid) {
|
if (!passwordValidation.valid) {
|
||||||
throw new Error(passwordValidation.message);
|
throw new Error(passwordValidation.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
await user.changePassword(currentPassword, newPassword);
|
await user.changePassword(currentPassword, newPassword);
|
||||||
|
|
||||||
// Revoke all refresh tokens to force re-login
|
// Revoke all refresh tokens to force re-login
|
||||||
await this.revokeAllUserTokens(userId);
|
await this.revokeAllUserTokens(userId);
|
||||||
|
|
||||||
console.log(`🔒 Password changed for user: ${user.email}`);
|
console.log(`🔒 Password changed for user: ${user.email}`);
|
||||||
return { message: 'Password changed successfully' };
|
return { message: 'Password changed successfully' };
|
||||||
}
|
}
|
||||||
@ -494,7 +516,7 @@ class AuthService {
|
|||||||
SET is_revoked = true, revoked_at = NOW()
|
SET is_revoked = true, revoked_at = NOW()
|
||||||
WHERE user_id = $1 AND is_revoked = false
|
WHERE user_id = $1 AND is_revoked = false
|
||||||
`;
|
`;
|
||||||
|
|
||||||
await database.query(query, [userId]);
|
await database.query(query, [userId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,7 +530,7 @@ class AuthService {
|
|||||||
(SELECT COUNT(*) FROM users WHERE last_login > NOW() - INTERVAL '24 hours') as users_24h,
|
(SELECT COUNT(*) FROM users WHERE last_login > NOW() - INTERVAL '24 hours') as users_24h,
|
||||||
(SELECT COUNT(*) FROM users WHERE created_at > NOW() - INTERVAL '7 days') as new_users_7d
|
(SELECT COUNT(*) FROM users WHERE created_at > NOW() - INTERVAL '7 days') as new_users_7d
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const result = await database.query(query);
|
const result = await database.query(query);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default function BusinessQuestionsScreen() {
|
|||||||
const [isLoadingQuestions, setIsLoadingQuestions] = useState(true);
|
const [isLoadingQuestions, setIsLoadingQuestions] = useState(true);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
const { selectedFeatures, setCurrentStep, projectName, projectType } = useProjectStore();
|
const { selectedFeatures, setCurrentStep, projectName, projectType } = useProjectStore();
|
||||||
|
|
||||||
// Load business questions when component mounts
|
// Load business questions when component mounts
|
||||||
@ -28,7 +28,15 @@ export default function BusinessQuestionsScreen() {
|
|||||||
console.log('🚀 Generating comprehensive business questions for integrated system:', selectedFeatures);
|
console.log('🚀 Generating comprehensive business questions for integrated system:', selectedFeatures);
|
||||||
|
|
||||||
// Call the new comprehensive endpoint
|
// Call the new comprehensive endpoint
|
||||||
const { getApiUrl } = require('../../../../../../config/urls');
|
// Prefer an environment-provided backend URL for frontend builds (REACT_APP_BACKEND_URL or NEXT_PUBLIC_BACKEND_URL).
|
||||||
|
const backendBase = (process.env.REACT_APP_BACKEND_URL || process.env.NEXT_PUBLIC_BACKEND_URL || '').replace(/\/$/, '') || '';
|
||||||
|
const getApiUrl = (endpoint) => {
|
||||||
|
const clean = endpoint.startsWith('/') ? endpoint.slice(1) : endpoint;
|
||||||
|
if (backendBase) return `${backendBase}/${clean}`;
|
||||||
|
// Fallback to relative path (assumes frontend is served with proxy to backend)
|
||||||
|
return `/${clean}`;
|
||||||
|
};
|
||||||
|
|
||||||
const response = await fetch(getApiUrl('api/v1/generate-comprehensive-business-questions'), {
|
const response = await fetch(getApiUrl('api/v1/generate-comprehensive-business-questions'), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -51,14 +59,14 @@ export default function BusinessQuestionsScreen() {
|
|||||||
|
|
||||||
if (data.success && data.data.businessQuestions) {
|
if (data.success && data.data.businessQuestions) {
|
||||||
setBusinessQuestions(data.data.businessQuestions);
|
setBusinessQuestions(data.data.businessQuestions);
|
||||||
|
|
||||||
// Initialize answers object
|
// Initialize answers object
|
||||||
const initialAnswers = {};
|
const initialAnswers = {};
|
||||||
data.data.businessQuestions.forEach((question, index) => {
|
data.data.businessQuestions.forEach((question, index) => {
|
||||||
initialAnswers[index] = '';
|
initialAnswers[index] = '';
|
||||||
});
|
});
|
||||||
setBusinessAnswers(initialAnswers);
|
setBusinessAnswers(initialAnswers);
|
||||||
|
|
||||||
console.log(`🎯 Generated ${data.data.businessQuestions.length} comprehensive questions for integrated system`);
|
console.log(`🎯 Generated ${data.data.businessQuestions.length} comprehensive questions for integrated system`);
|
||||||
} else {
|
} else {
|
||||||
setError('Failed to generate comprehensive business questions');
|
setError('Failed to generate comprehensive business questions');
|
||||||
@ -85,7 +93,7 @@ export default function BusinessQuestionsScreen() {
|
|||||||
|
|
||||||
// Validate that at least some questions are answered
|
// Validate that at least some questions are answered
|
||||||
const answeredQuestions = Object.values(businessAnswers).filter(answer => answer.trim()).length;
|
const answeredQuestions = Object.values(businessAnswers).filter(answer => answer.trim()).length;
|
||||||
|
|
||||||
if (answeredQuestions === 0) {
|
if (answeredQuestions === 0) {
|
||||||
alert('Please answer at least one question before proceeding.');
|
alert('Please answer at least one question before proceeding.');
|
||||||
return;
|
return;
|
||||||
@ -100,13 +108,13 @@ export default function BusinessQuestionsScreen() {
|
|||||||
businessQuestions: businessQuestions,
|
businessQuestions: businessQuestions,
|
||||||
businessAnswers: businessAnswers,
|
businessAnswers: businessAnswers,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|
||||||
// For backward compatibility with tech-stack-selector
|
// For backward compatibility with tech-stack-selector
|
||||||
featureName: `${projectName} - Integrated System`,
|
featureName: `${projectName} - Integrated System`,
|
||||||
description: `Complete ${projectType} system with ${selectedFeatures.length} integrated features`,
|
description: `Complete ${projectType} system with ${selectedFeatures.length} integrated features`,
|
||||||
requirements: selectedFeatures.flatMap(f => f.requirements || []),
|
requirements: selectedFeatures.flatMap(f => f.requirements || []),
|
||||||
complexity: selectedFeatures.some(f => f.complexity === 'high') ? 'high' :
|
complexity: selectedFeatures.some(f => f.complexity === 'high') ? 'high' :
|
||||||
selectedFeatures.some(f => f.complexity === 'medium') ? 'medium' : 'low',
|
selectedFeatures.some(f => f.complexity === 'medium') ? 'medium' : 'low',
|
||||||
logicRules: selectedFeatures.flatMap(f => f.logicRules || [])
|
logicRules: selectedFeatures.flatMap(f => f.logicRules || [])
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -129,10 +137,10 @@ export default function BusinessQuestionsScreen() {
|
|||||||
console.log('✅ Tech stack recommendations received:', techRecommendations);
|
console.log('✅ Tech stack recommendations received:', techRecommendations);
|
||||||
|
|
||||||
// Store results in project store
|
// Store results in project store
|
||||||
useProjectStore.setState({
|
useProjectStore.setState({
|
||||||
finalProjectData: completeData,
|
finalProjectData: completeData,
|
||||||
techStackRecommendations: techRecommendations,
|
techStackRecommendations: techRecommendations,
|
||||||
businessQuestionsCompleted: true
|
businessQuestionsCompleted: true
|
||||||
});
|
});
|
||||||
|
|
||||||
// Move to summary step to show recommendations
|
// Move to summary step to show recommendations
|
||||||
@ -206,7 +214,7 @@ export default function BusinessQuestionsScreen() {
|
|||||||
<span className="text-blue-600 text-lg">💡</span>
|
<span className="text-blue-600 text-lg">💡</span>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-blue-800 text-sm">
|
<p className="text-blue-800 text-sm">
|
||||||
<strong>Tip:</strong> Answer as many questions as possible for better technology recommendations.
|
<strong>Tip:</strong> Answer as many questions as possible for better technology recommendations.
|
||||||
You can skip questions you're unsure about. These questions consider your entire system, not individual features.
|
You can skip questions you're unsure about. These questions consider your entire system, not individual features.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -224,7 +232,7 @@ export default function BusinessQuestionsScreen() {
|
|||||||
<span>{question}</span>
|
<span>{question}</span>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
value={businessAnswers[index] || ''}
|
value={businessAnswers[index] || ''}
|
||||||
onChange={(e) => handleAnswerChange(index, e.target.value)}
|
onChange={(e) => handleAnswerChange(index, e.target.value)}
|
||||||
@ -260,15 +268,14 @@ export default function BusinessQuestionsScreen() {
|
|||||||
>
|
>
|
||||||
← Back to Features
|
← Back to Features
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={isSubmitting || Object.values(businessAnswers).filter(answer => answer.trim()).length === 0}
|
disabled={isSubmitting || Object.values(businessAnswers).filter(answer => answer.trim()).length === 0}
|
||||||
className={`px-6 py-2 rounded-md font-medium transition-colors ${
|
className={`px-6 py-2 rounded-md font-medium transition-colors ${isSubmitting || Object.values(businessAnswers).filter(answer => answer.trim()).length === 0
|
||||||
isSubmitting || Object.values(businessAnswers).filter(answer => answer.trim()).length === 0
|
|
||||||
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
: 'bg-green-600 text-white hover:bg-green-700'
|
: 'bg-green-600 text-white hover:bg-green-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user