How I built a full-stack authentication system with JWT, React, and Monero paymentsβall on a VPS.
π± The Journey Continues
MyZubster is an open-source decentralized marketplace that uses Monero (XMR) for private payments. After deploying the core infrastructure (Gateway, Marketplace, Security Bot), I focused on implementing a complete authentication system and integrating it with the React frontend.
This post covers:
JWT authentication implementation
User registration and login system
React frontend integration
Production deployment with Nginx and HTTPS
πΊοΈ What We Built
Feature Technology Status
JWT Authentication Node.js + jsonwebtoken β
Complete
User Registration bcryptjs + MongoDB β
Complete
User Login JWT + bcrypt β
Complete
Protected Routes JWT middleware β
Complete
React Frontend React + Axios β
Complete
HTTPS Let's Encrypt + Nginx β
Complete
π§ Step 1: Backend Authentication
1.1 User Model (MongoDB)
javascript
// models/User.js
const UserSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3,
maxlength: 30
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true
},
password: {
type: String,
required: true,
minlength: 6
},
role: {
type: String,
enum: ['user', 'admin', 'moderator'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
}
}, { timestamps: true });
1.2 Authentication Routes
javascript
// routes/auth.js
router.post('/register', async (req, res) => {
const { username, email, password, name } = req.body;
// Check if user exists
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(409).json({
success: false,
error: 'Username or email already in use'
});
}
// Hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);
// Create user
const user = new User({
username,
email,
password: hashedPassword,
name: name || username
});
await user.save();
// Generate JWT
const token = jwt.sign(
{ id: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
success: true,
token,
user: {
id: user._id,
username: user.username,
email: user.email,
name: user.name,
role: user.role
}
});
});
router.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
return res.status(401).json({
success: false,
error: 'Invalid credentials'
});
}
const token = jwt.sign(
{ id: user._id, username: user.username, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
success: true,
token,
user: {
id: user._id,
username: user.username,
email: user.email,
name: user.name,
role: user.role
}
});
});
1.3 JWT Middleware
javascript
// server.js
const authenticate = (req, res, next) => {
const publicRoutes = [
'/api/health',
'/api/auth/login',
'/api/auth/register',
'/api/auth/verify'
];
if (publicRoutes.includes(req.path)) {
return next();
}
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: 'Token required' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch {
return res.status(401).json({ error: 'Invalid or expired token' });
}
};
// Apply to protected routes
app.use('/api', authenticate);
π¨ Step 2: Frontend Integration
2.1 API Service
javascript
// services/api.js
import axios from 'axios';
const API_URL = 'https://myzubster.com/api';
const api = axios.create({
baseURL: API_URL,
headers: { 'Content-Type': 'application/json' }
});
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = Bearer ${token};
}
return config;
});
export default api;
2.2 Auth Service
javascript
// services/auth.js
import api from './api';
export const register = async (userData) => {
const response = await api.post('/auth/register', userData);
return response.data;
};
export const login = async (credentials) => {
const response = await api.post('/auth/login', credentials);
return response.data;
};
export const logout = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
};
export const getCurrentUser = () => {
const user = localStorage.getItem('user');
return user ? JSON.parse(user) : null;
};
2.3 Auth Component (React)
javascript
// components/Auth.jsx
const Auth = ({ onLogin }) => {
const [isLogin, setIsLogin] = useState(true);
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
name: ''
});
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setLoading(true);
try {
let response;
if (isLogin) {
response = await login({
email: formData.email,
password: formData.password
});
} else {
response = await register(formData);
}
if (response.success) {
localStorage.setItem('token', response.token);
localStorage.setItem('user', JSON.stringify(response.user));
onLogin(response.user);
}
} catch (err) {
setError(err.response?.data?.error || 'Authentication failed');
} finally {
setLoading(false);
}
};
return (
<div className="auth-container">
<h2>{isLogin ? 'Login' : 'Register'}</h2>
{error && <div className="error">{error}</div>}
<form onSubmit={handleSubmit}>
{!isLogin && (
<>
<input
type="text"
name="username"
placeholder="Username"
value={formData.username}
onChange={handleChange}
required
/>
<input
type="text"
name="name"
placeholder="Full name"
value={formData.name}
onChange={handleChange}
/>
</>
)}
<input
type="email"
name="email"
placeholder="Email"
value={formData.email}
onChange={handleChange}
required
/>
<input
type="password"
name="password"
placeholder="Password"
value={formData.password}
onChange={handleChange}
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Loading...' : (isLogin ? 'Login' : 'Register')}
</button>
</form>
<p>
{isLogin ? "Don't have an account?" : "Already have an account?"}
<button onClick={() => setIsLogin(!isLogin)}>
{isLogin ? 'Register' : 'Login'}
</button>
</p>
</div>
);
};
π Step 3: Production Deployment
3.1 Nginx Configuration (HTTPS)
nginx
server {
listen 80;
server_name myzubster.com www.myzubster.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name myzubster.com www.myzubster.com;
ssl_certificate /etc/letsencrypt/live/myzubster.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/myzubster.com/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
root /var/www/myzubster-frontend;
index index.html;
# Security Headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://localhost:3002/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
3.2 Deploy Commands
bash
Build frontend
cd ~/myzubster-frontend
npm run build
Deploy to Nginx
sudo cp -r dist/* /var/www/myzubster-frontend/
sudo chown -R www-data:www-data /var/www/myzubster-frontend/
Restart services
sudo systemctl reload nginx
Start gateway
cd ~/MyZubsterGateway
export PORT=3002
nohup node server.js > /var/log/gateway.log 2>&1 &
β Results
After completing the deployment, the system is fully functional:
Component Status URL
Frontend β
Live https://myzubster.com
API Gateway β
Live https://myzubster.com/api
Registration β
Working POST /api/auth/register
Login β
Working POST /api/auth/login
Protected Routes β
Working JWT middleware
HTTPS β
Active Let's Encrypt
Security Headers Verified
text
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=31536000; includeSubDomains
Content-Security-Policy: default-src 'self'...
π‘ Lessons Learned
JWT is simple yet powerful β Stateless authentication scales well.
Rate limiting is essential β Protects against brute-force attacks.
Environment variables are critical β Keep secrets out of code.
HTTPS is non-negotiable β Let's Encrypt makes it easy.
React + Axios β Clean API integration with interceptors.
Nginx is a great reverse proxy β Handles SSL and API routing efficiently.
Security headers β CSP and HSTS protect against common attacks.
π Repository & Documentation
All code is available on GitHub:
Repository Description Stars
MyZubsterGateway Monero payment engine β 3
myzubster-frontend React frontend β 0
MyZubster-Marketplace Backend API β 1
π€ Contribute
If you're interested in blockchain, privacy-first payments, or decentralized markets, contribute to the repositories on GitHub or open an issue to discuss new features.
π Connect with Me
Blog: DEV.to - Daniel Ioni
X (Twitter): @myzubster
LinkedIn: Daniel Ioni
GitHub: DanielIoni-creator
TikTok: @h4x0r_23
Built with β€οΈ for Monero, open source, and the global community.
Top comments (0)