claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
710 lines (709 loc) • 28.3 kB
JavaScript
/**
* User Service
*
* Comprehensive user management service with authentication, authorization,
* and security features for the CFN Loop system.
*/ import { v4 as uuidv4 } from 'uuid';
import { UserRole, UserStatus } from '../types/user.js';
import { PasswordUtils, JWTUtils, AuthRateLimit } from '../middleware/authentication.js';
import { AuthMiddleware, RBACEnforcer } from '../middleware/auth-middleware.js';
import { createLogger } from '../lib/logging.js';
import { StandardError, ErrorCode } from '../lib/errors.js';
const logger = createLogger('user-service');
/**
* User Service Class
*
* Manages user lifecycle, authentication, and authorization.
*/ export class UserService {
db;
authMiddleware;
rbacEnforcer;
config;
constructor(db, config){
this.db = db;
this.config = config;
this.authMiddleware = new AuthMiddleware(config.jwtSecret);
this.rbacEnforcer = new RBACEnforcer(this.authMiddleware);
}
/**
* Initialize the user service
* Creates necessary tables and indexes if they don't exist
*/ async initialize() {
try {
const sqliteAdapter = this.db.getAdapter('sqlite');
// Create users table
await sqliteAdapter.execute(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
status TEXT NOT NULL DEFAULT 'pending_verification',
password_hash TEXT NOT NULL,
salt TEXT NOT NULL,
profile_json TEXT NOT NULL,
privacy_json TEXT NOT NULL,
preferences_json TEXT NOT NULL,
security_json TEXT NOT NULL,
stats_json TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
last_login_at INTEGER,
email_verified_at INTEGER,
phone_verified_at INTEGER
)
`);
// Create refresh tokens table
await sqliteAdapter.execute(`
CREATE TABLE IF NOT EXISTS refresh_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
last_used_at INTEGER,
is_revoked BOOLEAN DEFAULT FALSE,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
// Create password reset tokens table
await sqliteAdapter.execute(`
CREATE TABLE IF NOT EXISTS password_reset_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token_hash TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
used_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
// Create email verification tokens table
await sqliteAdapter.execute(`
CREATE TABLE IF NOT EXISTS email_verification_tokens (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
email TEXT NOT NULL,
token_hash TEXT NOT NULL,
expires_at INTEGER NOT NULL,
created_at INTEGER NOT NULL,
verified_at INTEGER,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
`);
// Create indexes
await sqliteAdapter.execute('CREATE INDEX IF NOT EXISTS idx_users_email ON users (email)');
await sqliteAdapter.execute('CREATE INDEX IF NOT EXISTS idx_users_username ON users (username)');
await sqliteAdapter.execute('CREATE INDEX IF NOT EXISTS idx_users_role ON users (role)');
await sqliteAdapter.execute('CREATE INDEX IF NOT EXISTS idx_users_status ON users (status)');
await sqliteAdapter.execute('CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens (user_id)');
await sqliteAdapter.execute('CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens (expires_at)');
logger.info('User service initialized successfully');
} catch (error) {
logger.error('Failed to initialize user service:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to initialize user service', {}, error);
}
}
/**
* Register a new user
*/ async register(request) {
const { username, email, password, firstName, lastName, displayName } = request;
try {
// Validate input
this.validateEmail(email);
this.validateUsername(username);
this.validatePassword(password);
// Check if user already exists
const existingUser = await this.findUserByEmailOrUsername(email, username);
if (existingUser) {
throw new StandardError(ErrorCode.CONFLICT, existingUser.email === email ? 'Email already registered' : 'Username already taken', {
field: existingUser.email === email ? 'email' : 'username'
});
}
// Hash password
const salt = await PasswordUtils.generateSalt();
const passwordHash = await PasswordUtils.hashPassword(password, this.config.bcryptRounds);
// Create user
const now = Math.floor(Date.now() / 1000);
const userId = uuidv4();
const user = {
id: userId,
username,
email,
role: UserRole.USER,
status: this.config.emailVerificationRequired ? UserStatus.PENDING_VERIFICATION : UserStatus.ACTIVE,
profile: {
firstName,
lastName,
displayName: displayName || `${firstName} ${lastName}`
},
privacy: {
profileVisibility: 'public',
allowFriendRequests: true,
allowMessages: true,
allowStoryTagging: true,
allowLocationSharing: false,
showOnlineStatus: true
},
preferences: {
theme: 'auto',
language: 'en',
timezone: 'UTC',
emailNotifications: true,
pushNotifications: true,
autoSaveStories: true,
storyVisibilityDefault: 'private'
},
security: {
email,
emailVerified: !this.config.emailVerificationRequired,
twoFactorEnabled: false,
loginAttempts: 0
},
stats: {
storiesCount: 0,
friendsCount: 0,
followersCount: 0,
followingCount: 0
},
createdAt: new Date(now * 1000),
updatedAt: new Date(now * 1000)
};
// Save user to database
await this.saveUser(user, passwordHash, salt);
// Create email verification token if required
if (this.config.emailVerificationRequired) {
await this.createEmailVerificationToken(userId, email);
}
// Generate tokens
const tokens = await this.generateAuthTokens(user);
// Log user registration
logger.info('User registered successfully', {
userId,
username,
email
});
return {
user: this.toPublicUser(user),
tokens
};
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('User registration failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'User registration failed', {}, error);
}
}
/**
* Authenticate user and generate tokens
*/ async login(request) {
const { email, password, rememberMe } = request;
try {
// Check rate limiting
if (AuthRateLimit.hasExceededAttempts(email, this.config.maxLoginAttempts, this.config.loginLockoutMs)) {
throw new StandardError(ErrorCode.TOO_MANY_REQUESTS, 'Too many login attempts. Please try again later.', {
retryAfter: Math.ceil(this.config.loginLockoutMs / 1000)
});
}
// Find user by email
const user = await this.findUserByEmail(email);
if (!user) {
AuthRateLimit.hasExceededAttempts(email, this.config.maxLoginAttempts, this.config.loginLockoutMs);
throw new StandardError(ErrorCode.NOT_FOUND, 'Invalid email or password');
}
// Check user status
if (user.status === UserStatus.SUSPENDED) {
throw new StandardError(ErrorCode.FORBIDDEN, 'Account suspended. Please contact support.');
}
if (user.status === UserStatus.INACTIVE) {
throw new StandardError(ErrorCode.FORBIDDEN, 'Account inactive. Please contact support.');
}
// Get user credentials
const credentials = await this.getUserCredentials(user.id);
if (!credentials) {
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'User credentials not found');
}
// Verify password
const isPasswordValid = await PasswordUtils.comparePassword(password, credentials.passwordHash);
if (!isPasswordValid) {
AuthRateLimit.hasExceededAttempts(email, this.config.maxLoginAttempts, this.config.loginLockoutMs);
// Update failed login attempts
await this.updateFailedLoginAttempts(user.id, user.security.loginAttempts + 1);
throw new StandardError(ErrorCode.NOT_FOUND, 'Invalid email or password');
}
// Clear failed login attempts
AuthRateLimit.clearAttempts(email);
await this.updateFailedLoginAttempts(user.id, 0);
// Update last login
await this.updateLastLogin(user.id);
// Generate tokens
const tokens = await this.generateAuthTokens(user, rememberMe);
// Log successful login
logger.info('User logged in successfully', {
userId: user.id,
email
});
return {
user: this.toPublicUser(user),
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresIn: tokens.expiresIn
};
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Login failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Login failed', {}, error);
}
}
/**
* Refresh access token using refresh token
*/ async refreshToken(refreshToken) {
try {
// Validate refresh token
const payload = JWTUtils.verifyToken(refreshToken, this.config.jwtSecret);
if (payload.type !== 'refresh') {
throw new StandardError(ErrorCode.INVALID_TOKEN, 'Invalid refresh token');
}
// Check if refresh token exists and is not revoked
const tokenRecord = await this.findRefreshToken(payload.jti, payload.userId);
if (!tokenRecord || tokenRecord.isRevoked) {
throw new StandardError(ErrorCode.INVALID_TOKEN, 'Invalid or revoked refresh token');
}
// Find user
const user = await this.findUserById(payload.userId);
if (!user) {
throw new StandardError(ErrorCode.NOT_FOUND, 'User not found');
}
// Check user status
if (user.status !== UserStatus.ACTIVE) {
throw new StandardError(ErrorCode.FORBIDDEN, 'Account is not active');
}
// Revoke old refresh token
await this.revokeRefreshToken(payload.jti);
// Generate new tokens
const tokens = await this.generateAuthTokens(user);
// Update last used timestamp
await this.updateRefreshTokenLastUsed(payload.jti);
logger.info('Token refreshed successfully', {
userId: user.id
});
return tokens;
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Token refresh failed:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Token refresh failed', {}, error);
}
}
/**
* Logout user and revoke tokens
*/ async logout(refreshToken) {
try {
// Validate refresh token
const payload = JWTUtils.decodeToken(refreshToken);
if (!payload || payload.type !== 'refresh') {
return; // Invalid token, just return success
}
// Revoke refresh token
if (payload.jti) {
await this.revokeRefreshToken(payload.jti);
}
logger.info('User logged out successfully', {
userId: payload.userId
});
} catch (error) {
logger.error('Logout failed:', error);
// Don't throw error on logout to prevent client issues
}
}
/**
* Get user by ID
*/ async getUserById(userId) {
try {
const user = await this.findUserById(userId);
return user ? this.toPublicUser(user) : null;
} catch (error) {
logger.error('Failed to get user by ID:', error);
return null;
}
}
/**
* Update user profile
*/ async updateUserProfile(userId, updates) {
try {
const user = await this.findUserById(userId);
if (!user) {
throw new StandardError(ErrorCode.NOT_FOUND, 'User not found');
}
// Update user
const updatedUser = {
...user,
profile: {
...user.profile,
...updates.profile
},
privacy: {
...user.privacy,
...updates.privacy
},
preferences: {
...user.preferences,
...updates.preferences
},
updatedAt: new Date()
};
await this.saveUser(updatedUser);
logger.info('User profile updated successfully', {
userId
});
return this.toPublicUser(updatedUser);
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Failed to update user profile:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to update user profile', {}, error);
}
}
/**
* Change user password
*/ async changePassword(userId, currentPassword, newPassword) {
try {
// Validate new password
this.validatePassword(newPassword);
// Get current credentials
const credentials = await this.getUserCredentials(userId);
if (!credentials) {
throw new StandardError(ErrorCode.NOT_FOUND, 'User not found');
}
// Verify current password
const isCurrentPasswordValid = await PasswordUtils.comparePassword(currentPassword, credentials.passwordHash);
if (!isCurrentPasswordValid) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Current password is incorrect');
}
// Hash new password
const salt = await PasswordUtils.generateSalt();
const newPasswordHash = await PasswordUtils.hashPassword(newPassword, this.config.bcryptRounds);
// Update password
await this.updateUserPassword(userId, newPasswordHash, salt);
// Revoke all refresh tokens for this user
await this.revokeAllUserRefreshTokens(userId);
logger.info('Password changed successfully', {
userId
});
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
logger.error('Failed to change password:', error);
throw new StandardError(ErrorCode.INTERNAL_ERROR, 'Failed to change password', {}, error);
}
}
/**
* Validate JWT token and return user context
*/ async validateToken(token) {
try {
return this.authMiddleware.validateToken(token);
} catch (error) {
throw new StandardError(ErrorCode.INVALID_TOKEN, 'Invalid authentication token', {}, error);
}
}
/**
* Check if user has permission for an operation
*/ async checkPermission(userContext, operation) {
try {
return this.rbacEnforcer.hasPermission(userContext, operation);
} catch (error) {
logger.error('Permission check failed:', error);
return false;
}
}
/**
* Enforce permission check - throws if user lacks permission
*/ async enforcePermission(userContext, operation, skillId) {
try {
this.rbacEnforcer.enforcePermission(userContext, operation, skillId);
} catch (error) {
if (error instanceof StandardError) {
throw error;
}
throw new StandardError(ErrorCode.FORBIDDEN, 'Permission check failed', {}, error);
}
}
// Private helper methods
validateEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Invalid email format');
}
}
validateUsername(username) {
if (username.length < 3 || username.length > 30) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Username must be between 3 and 30 characters');
}
const usernameRegex = /^[a-zA-Z0-9_-]+$/;
if (!usernameRegex.test(username)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Username can only contain letters, numbers, underscores, and hyphens');
}
}
validatePassword(password) {
if (!PasswordUtils.validatePasswordStrength(password)) {
throw new StandardError(ErrorCode.VALIDATION_FAILED, 'Password must be at least 8 characters long and contain uppercase, lowercase, number, and special character');
}
}
async generateAuthTokens(user, rememberMe = false) {
const expiresIn = rememberMe ? '30d' : this.config.jwtExpiration;
const refreshExpiresIn = rememberMe ? '90d' : this.config.refreshExpiration;
// Generate access token
const accessTokenPayload = {
userId: user.id,
email: user.email,
role: user.role,
type: 'access'
};
const accessToken = JWTUtils.generateToken(accessTokenPayload, this.config.jwtSecret, expiresIn);
// Generate refresh token
const refreshTokenId = uuidv4();
const refreshTokenPayload = {
userId: user.id,
email: user.email,
role: user.role,
type: 'refresh',
jti: refreshTokenId
};
const refreshToken = JWTUtils.generateToken(refreshTokenPayload, this.config.jwtSecret, refreshExpiresIn);
// Save refresh token to database
const now = Math.floor(Date.now() / 1000);
const refreshTokenHash = await PasswordUtils.hashPassword(refreshToken, 10);
const expiresAt = now + (rememberMe ? 90 * 24 * 60 * 60 : 7 * 24 * 60 * 60); // 90 days or 7 days
await this.saveRefreshToken(refreshTokenId, user.id, refreshTokenHash, expiresAt);
// Calculate expiresIn in seconds
const expiresInSeconds = rememberMe ? 30 * 24 * 60 * 60 : 24 * 60 * 60; // 30 days or 24 hours
return {
accessToken,
refreshToken,
expiresIn: expiresInSeconds
};
}
toPublicUser(user) {
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
status: user.status,
profile: {
displayName: user.profile.displayName,
firstName: user.profile.firstName,
lastName: user.profile.lastName,
bio: user.profile.bio,
avatar: user.profile.avatar
},
security: {
emailVerified: user.security.emailVerified,
twoFactorEnabled: user.security.twoFactorEnabled
}
};
}
// Database helper methods
async findUserById(userId) {
const adapter = this.db.getAdapter('sqlite');
const row = await adapter.get('SELECT * FROM users WHERE id = ?', [
userId
]);
return row ? this.rowToUser(row) : null;
}
async findUserByEmail(email) {
const adapter = this.db.getAdapter('sqlite');
const row = await adapter.get('SELECT * FROM users WHERE email = ?', [
email
]);
return row ? this.rowToUser(row) : null;
}
async findUserByEmailOrUsername(email, username) {
const adapter = this.db.getAdapter('sqlite');
const row = await adapter.get('SELECT * FROM users WHERE email = ? OR username = ?', [
email,
username
]);
return row ? this.rowToUser(row) : null;
}
async getUserCredentials(userId) {
const adapter = this.db.getAdapter('sqlite');
const row = await adapter.get('SELECT password_hash, salt FROM users WHERE id = ?', [
userId
]);
return row ? {
passwordHash: row.password_hash,
salt: row.salt
} : null;
}
async saveUser(user, passwordHash, salt) {
const adapter = this.db.getAdapter('sqlite');
const now = Math.floor(Date.now() / 1000);
await adapter.execute(`
INSERT OR REPLACE INTO users (
id, username, email, role, status, password_hash, salt,
profile_json, privacy_json, preferences_json, security_json, stats_json,
created_at, updated_at, last_login_at, email_verified_at, phone_verified_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, [
user.id,
user.username,
user.email,
user.role,
user.status,
passwordHash,
salt,
JSON.stringify(user.profile),
JSON.stringify(user.privacy),
JSON.stringify(user.preferences),
JSON.stringify(user.security),
JSON.stringify(user.stats),
Math.floor(user.createdAt.getTime() / 1000),
now,
user.updatedAt ? Math.floor(user.updatedAt.getTime() / 1000) : now,
user.security.emailVerifiedAt ? Math.floor(user.security.emailVerifiedAt.getTime() / 1000) : null,
user.security.phoneVerifiedAt ? Math.floor(user.security.phoneVerifiedAt.getTime() / 1000) : null
]);
}
async updateFailedLoginAttempts(userId, attempts) {
const adapter = this.db.getAdapter('sqlite');
const lockedUntil = attempts >= this.config.maxLoginAttempts ? Math.floor(Date.now() / 1000) + Math.floor(this.config.loginLockoutMs / 1000) : null;
await adapter.execute(`
UPDATE users
SET security_json = json_set(
json_set(security_json, '$.loginAttempts', ?),
'$.lockedUntil',
?
)
WHERE id = ?
`, [
attempts,
lockedUntil,
userId
]);
}
async updateLastLogin(userId) {
const adapter = this.db.getAdapter('sqlite');
const now = Math.floor(Date.now() / 1000);
await adapter.execute(`
UPDATE users
SET last_login_at = ?, updated_at = ?
WHERE id = ?
`, [
now,
now,
userId
]);
}
async updateRefreshTokenLastUsed(tokenId) {
const adapter = this.db.getAdapter('sqlite');
const now = Math.floor(Date.now() / 1000);
await adapter.execute(`
UPDATE refresh_tokens
SET last_used_at = ?
WHERE id = ?
`, [
now,
tokenId
]);
}
async saveRefreshToken(tokenId, userId, tokenHash, expiresAt) {
const adapter = this.db.getAdapter('sqlite');
const now = Math.floor(Date.now() / 1000);
await adapter.execute(`
INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at, created_at)
VALUES (?, ?, ?, ?, ?)
`, [
tokenId,
userId,
tokenHash,
expiresAt,
now
]);
}
async findRefreshToken(tokenId, userId) {
const adapter = this.db.getAdapter('sqlite');
const row = await adapter.get('SELECT id, is_revoked FROM refresh_tokens WHERE id = ? AND user_id = ? AND expires_at > ?', [
tokenId,
userId,
Math.floor(Date.now() / 1000)
]);
return row;
}
async revokeRefreshToken(tokenId) {
const adapter = this.db.getAdapter('sqlite');
await adapter.execute('UPDATE refresh_tokens SET is_revoked = TRUE WHERE id = ?', [
tokenId
]);
}
async revokeAllUserRefreshTokens(userId) {
const adapter = this.db.getAdapter('sqlite');
await adapter.execute('UPDATE refresh_tokens SET is_revoked = TRUE WHERE user_id = ?', [
userId
]);
}
async updateUserPassword(userId, passwordHash, salt) {
const adapter = this.db.getAdapter('sqlite');
const now = Math.floor(Date.now() / 1000);
await adapter.execute(`
UPDATE users
SET password_hash = ?, salt = ?, updated_at = ?,
security_json = json_set(security_json, '$.lastPasswordChange', ?)
WHERE id = ?
`, [
passwordHash,
salt,
now,
now,
userId
]);
}
async createEmailVerificationToken(userId, email) {
const adapter = this.db.getAdapter('sqlite');
const tokenId = uuidv4();
const token = uuidv4();
const tokenHash = await PasswordUtils.hashPassword(token, 10);
const expiresAt = Math.floor(Date.now() / 1000) + 24 * 60 * 60; // 24 hours
await adapter.execute(`
INSERT INTO email_verification_tokens (id, user_id, email, token_hash, expires_at, created_at)
VALUES (?, ?, ?, ?, ?, ?)
`, [
tokenId,
userId,
email,
tokenHash,
expiresAt,
Math.floor(Date.now() / 1000)
]);
// In a real implementation, send email with token
logger.info('Email verification token created', {
userId,
email,
token
});
}
rowToUser(row) {
return {
id: row.id,
username: row.username,
email: row.email,
role: row.role,
status: row.status,
profile: JSON.parse(row.profile_json),
privacy: JSON.parse(row.privacy_json),
preferences: JSON.parse(row.preferences_json),
security: JSON.parse(row.security_json),
stats: JSON.parse(row.stats_json),
createdAt: new Date(row.created_at * 1000),
updatedAt: new Date(row.updated_at * 1000)
};
}
}
export default UserService;
//# sourceMappingURL=user-service.js.map