A secure, production-ready web application for book lovers to discuss and chat in real-time. Built with Node.js, Express, SQLite3, and Socket.IO.
Live Site: https://bsturdivant.com
Authentication & Security
- Secure password hashing using Argon2
- Account lockout after 5 failed login attempts (15-minute lockout period)
- HTTPS encryption via Nginx Proxy Manager with Let's Encrypt SSL
- Session management with SQLite-based session storage
- IP address logging for all login attempts
User Management
- Email-based user accounts (unique email required)
- Display names separate from usernames
- User profile page with ability to:
- Change password (requires current password verification)
- Change email address (requires current password verification)
- Change display name
- Customize profile name color (displays in comments)
Password Recovery
- "Forgot Password" functionality
- Secure, time-limited reset tokens (10-minute expiration)
- Email-based password reset via MailerSend API
- Token invalidation after use
Real-Time Chat
- Socket.IO-based real-time chat system
- Messages stored in SQLite database
- Display names and profile colors shown in chat
- Timestamps for all messages
- Authentication required to access chat
Comment System
- Comment posting with user attribution
- Display names and profile colors shown in comments - Colors don't always update after the comment has already been left
- Timestamps for all comments
- Comment Pagination - Only showing 10 comments per page
- Maximum comment length validation (500 characters) - Tried to make this work in time for the assignment, but its not quite there
Comment Truncation
- Code implemented for 200-character preview with "Read More" functionality
- Backend processing complete but display not working as expected
Profile Color Updates
- Profile name color customization works for new comments
- Historical comments do not reflect updated colors consistently
- No "success" message when updating color
Display Name Stuff
- When changing display name in the profile page, it doesn't go through the same required checks that happen during registration - working on fixing this by sunday evening
- Display name is shown to user except when they're in the profile page - Then it shows up as "object, object". Trying to figure out why this happens only on the profile page and nowhere else
- Docker and Docker Compose installed
- Domain name with DNS pointing to your server
- MailerSend API account (for password recovery emails)
- Clone the repository
git clone
cd -
Create environment file
Create
nodejs/modules/.envwith the following:
MAILERSEND_API_KEY=your_mailersend_api_key_here
-
Configure Docker Compose
The
docker-compose.ymlis already configured. Key services:backend-nodejs: Node.js application (port 3000)nginx-proxy-manager: Reverse proxy for HTTPS (ports 80, 443, 5001)
-
Start the application
docker-compose up -d --build-
Configure Nginx Proxy Manager
Access admin panel at
http://YOUR_SERVER_IP:5001Default credentials:
- Email:
admin@example.com - Password:
changeme
Add a Proxy Host:
- Domain:
your-domain.com - Forward Hostname:
backend-nodejs - Forward Port:
3000 - Enable "Websockets Support" (required for Socket.IO)
- Request SSL certificate (Let's Encrypt)
- Enable "Force SSL"
- Email:
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL, -- Argon2 hashed
display_name TEXT NOT NULL UNIQUE,
profile_color TEXT NOT NULL DEFAULT '#000000',
account_lockout TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login DATETIME,
password_reset_token TEXT,
password_reset_expire INTEGER
)CREATE TABLE sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER REFERENCES users(id)
)CREATE TABLE comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER REFERENCES users(id),
comment TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)CREATE TABLE login_attempts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip_address TEXT NOT NULL,
username TEXT NOT NULL,
attempt_time DATETIME DEFAULT CURRENT_TIMESTAMP,
success INTEGER DEFAULT 0
)CREATE TABLE chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
userId INTEGER NOT NULL REFERENCES users(id),
message TEXT NOT NULL,
displayName TEXT NOT NULL,
created_at TEXT NOT NULL
)Required in nodejs/modules/.env:
| Variable | Description | Example |
|---|---|---|
MAILERSEND_API_KEY |
API key from MailerSend for password reset emails | mlsn.xxxxx |
- Hashing Algorithm: Argon2 (industry-standard, memory-hard)
- Password Requirements:
- Minimum 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
- Storage: Only hashed passwords stored in database
- Validation: Requirements enforced on registration and password changes
- Trigger: 5 failed login attempts
- Duration: 15-minute lockout
- Tracking: All attempts logged with IP address and timestamp
- Database Storage: Login attempts stored in
login_attemptstable - IP-based: Lockout applies per IP address + username combination
- Implementation: Nginx Proxy Manager with Let's Encrypt
- Certificate: Auto-renewed SSL certificates
- Force HTTPS: All HTTP traffic redirected to HTTPS
- Websocket Support: Enabled for Socket.IO real-time chat
- Storage: SQLite-based sessions (not in-memory)
- Middleware: express-session with custom SQLite store
- Security: Session cookie not accessible via JavaScript
- Expiration: 24-hour session lifetime
- Token Generation: Cryptographically secure random tokens (32 bytes)
- Expiration: 10-minute token lifetime
- One-time Use: Tokens invalidated after successful password reset
- Email Delivery: Sent via MailerSend API with reset link
POST /register
- Creates new user account
- Body:
{ username, email, password, display_name, profileColor } - Validates password requirements and unique email/username
- Returns: Redirect to login page
POST /login
- Authenticates user and creates session
- Body:
{ username, password } - Implements account lockout after failed attempts
- Returns: Redirect to comments page or error
GET /logout
- Destroys user session
- Returns: Redirect to home page
POST /forgot-password
- Generates password reset token and sends email
- Body:
{ email } - Returns: Success message (regardless of email existence for security)
POST /reset-password
- Resets password using valid token
- Body:
{ token, password, confirmPassword } - Validates token, password requirements, and password match
- Returns: Redirect to login with success message
GET /profile
- Displays user profile page
- Requires: Active session
- Returns: Profile page with user data
POST /profile
- Updates user password, email, or display name
- Body (password):
{ currentPassword, newPassword } - Body (email):
{ currentPasswordEmail, newEmail } - Body (display name):
{ displayName } - Requires: Current password verification for password/email changes
- Returns: Updated profile page with success/error message
POST /profile/color
- Updates user's profile name color
- Body:
{ profileColor } - Returns: Redirect to profile page
GET /comments
- Displays paginated comments
- Query params:
?page=<number>(default: 1) - Returns: Comment list with pagination controls
POST /comments
- Creates new comment
- Body:
{ comment } - Validates: User authentication, comment length (max 500 chars)
- Returns: Redirect to comments page
GET /chat
- Displays real-time chat interface
- Requires: Active session
- Returns: Chat page with message history
Event: 'connection'
- Triggered when user connects to chat
- Authenticates user via session
- Disconnects if not authenticated
Event: 'chat'
- Client → Server: Sends new chat message
- Body:
{ message } - Server → All Clients: Broadcasts message with user info
- Response:
{ user, message, timestamp }