Added api endpoints for user creation, created login and register components, and starrted modifying app.jsx to handle auth workflow
This commit is contained in:
parent
e603780812
commit
3daac43d76
@ -5,18 +5,41 @@ import SearchBar from './components/SearchBar';
|
|||||||
import ArticleList from './components/ArticleList';
|
import ArticleList from './components/ArticleList';
|
||||||
import ArticleDetail from './components/ArticleDetail';
|
import ArticleDetail from './components/ArticleDetail';
|
||||||
import ArticleEditor from './components/ArticleEdit';
|
import ArticleEditor from './components/ArticleEdit';
|
||||||
|
import Login from './components/Login';
|
||||||
|
import Registration from './components/Registration'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
|
const [currentUser, setCurrentUser] = useState(null);
|
||||||
|
const [token, setToken] = useState(null);
|
||||||
|
const [authView, setAuthView] = useState('login');
|
||||||
const [currentView, setCurrentView] = useState('list');
|
const [currentView, setCurrentView] = useState('list');
|
||||||
const [selectedArticle, setSelectedArticle] = useState(null);
|
const [selectedArticle, setSelectedArticle] = useState(null);
|
||||||
const [articles, setArticles] = useState([]);
|
const [articles, setArticles] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const savedToken = localStorage.getItem('token');
|
||||||
|
const savedUser = localStorage.getItem('user');
|
||||||
|
|
||||||
|
if (savedToken && savedUser) {
|
||||||
|
setToken(savedToken);
|
||||||
|
setCurrentUser(JSON.parse(savedUser));
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLoggedIn && token) {
|
||||||
|
fetchArticles();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchArticles = () => {
|
||||||
fetch('http://localhost:9000/api/articles')
|
fetch('http://localhost:9000/api/articles')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => setArticles(data))
|
.then(data => setArticles(data))
|
||||||
.catch(error => console.error('Error fetching articles:', error))
|
.catch(error => console.error('Error fetching articles:', error))
|
||||||
}, []);
|
};
|
||||||
|
|
||||||
const handleSearch = (query) => {
|
const handleSearch = (query) => {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
|
|||||||
107
client/kb-frontend/src/components/Login.css
Normal file
107
client/kb-frontend/src/components/Login.css
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box button {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box button:hover:not(:disabled) {
|
||||||
|
background-color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box button:disabled {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-auth {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button:hover:not(:disabled) {
|
||||||
|
color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button:disabled {
|
||||||
|
color: #95a5a6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
91
client/kb-frontend/src/components/Login.jsx
Normal file
91
client/kb-frontend/src/components/Login.jsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import './Login.css';
|
||||||
|
|
||||||
|
function Login({ onLoginSuccess, onSwitchToRegister }) {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:9000/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
password: password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
onLoginSuccess(data.user, data.token);
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Login failed');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Login error:', err);
|
||||||
|
setError('Failed to connect to server');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='login-container'>
|
||||||
|
<div className='login-box'>
|
||||||
|
<h2>Login to Knowledge Base</h2>
|
||||||
|
{error && <div className='error-message'>{error}</div>}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='username'>Username:</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='username'
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='password'>Password:</label>
|
||||||
|
<input
|
||||||
|
type='password'
|
||||||
|
id='password'
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type='submit' disabled={loading}>
|
||||||
|
{loading ? 'Logging in...': 'Login'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className='switch-auth'>
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='link-button'
|
||||||
|
onClick={onSwitchToRegister}
|
||||||
|
disabled={loading}
|
||||||
|
>Register here</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login;
|
||||||
107
client/kb-frontend/src/components/Registration.css
Normal file
107
client/kb-frontend/src/components/Registration.css
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
.registration-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-box {
|
||||||
|
background-color: #fff;
|
||||||
|
padding: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-box h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-box button[type="submit"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #27ae60;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-box button[type="submit"]:hover:not(:disabled) {
|
||||||
|
background-color: #229954;
|
||||||
|
}
|
||||||
|
|
||||||
|
.registration-box button[type="submit"]:disabled {
|
||||||
|
background-color: #95a5a6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background-color: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.switch-auth {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #3498db;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding: 0;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button:hover:not(:disabled) {
|
||||||
|
color: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-button:disabled {
|
||||||
|
color: #95a5a6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
144
client/kb-frontend/src/components/Registration.jsx
Normal file
144
client/kb-frontend/src/components/Registration.jsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import './Registration.css';
|
||||||
|
|
||||||
|
function Registration({ onRegistrationSuccess, onSwitchToLogin }) {
|
||||||
|
const [username, setUsername] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [displayName, setDisplayName] = useState('');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password.length < 8) {
|
||||||
|
setError('Password must be at least 8 characters');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:9000/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: username,
|
||||||
|
email: email,
|
||||||
|
password: password,
|
||||||
|
display_name: displayName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
onRegistrationSuccess(data.user, data.token);
|
||||||
|
} else {
|
||||||
|
setError(data.error || 'Registration failed');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Registration failed:', err);
|
||||||
|
setError('Failed to connect to server');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='registration-container'>
|
||||||
|
<div className='registration-box'>
|
||||||
|
<h2>Create Account</h2>
|
||||||
|
{error && <div className='error-message'>{error}</div>}
|
||||||
|
|
||||||
|
<form onSubmit={{handleSubmit}}>
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor="username">Username:</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='username'
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='email'>Email:</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='email'
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='display_name'>Display Name:</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
id='display_name'
|
||||||
|
value={displayName}
|
||||||
|
onChange={(e) => setDisplayName(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='password'>Password:</label>
|
||||||
|
<input
|
||||||
|
type='password'
|
||||||
|
id='password'
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='form-group'>
|
||||||
|
<label htmlFor='confirmPassword'>Confirm Password:</label>
|
||||||
|
<input
|
||||||
|
type='password'
|
||||||
|
id='confirmPassword'
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type='submit' disabled={loading}>
|
||||||
|
{loading ? 'Creating Account...' : 'Register'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className='switch-auth'>
|
||||||
|
Already have an account?
|
||||||
|
<button
|
||||||
|
type='button'
|
||||||
|
className='link-button'
|
||||||
|
onClick={onSwitchToLogin}
|
||||||
|
disabled={loading}
|
||||||
|
>Login Here</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Reguistration
|
||||||
80
server/auth.js
Normal file
80
server/auth.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const {getUserById} = require('./db');
|
||||||
|
|
||||||
|
const JWT_SECRET = 'THISISTOTALLYSECRETDONTLOOK';
|
||||||
|
const JWT_EXPIRATION = '24h';
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a JWT token for a user
|
||||||
|
* @param {Object}- User object from database
|
||||||
|
* @returns {string} - signed JWT token
|
||||||
|
*/
|
||||||
|
function generateToken(user) {
|
||||||
|
const payload = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
display_name: user.display_name,
|
||||||
|
auth_provider: user.auth_provider
|
||||||
|
};
|
||||||
|
|
||||||
|
const token = jwt.sign(payload, JWT_SECRET, {expiresIn: JWT_EXPIRATION});
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify and decode a JWT token
|
||||||
|
* @param {string} token - JWT token to verify
|
||||||
|
* @returns {Object} - Decoded token payload
|
||||||
|
* @throws {Error} - If token is invalid or expired
|
||||||
|
*/
|
||||||
|
function verifyToken(token) {
|
||||||
|
try {
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET);
|
||||||
|
return decoded;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error('Invalid or expired token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Express middleware to authenticate requests using JWT
|
||||||
|
* Extracts token from Authorization header, verfies it, and attaches user to req.user
|
||||||
|
*/
|
||||||
|
function authenticateToken(req, res, next) {
|
||||||
|
const authHeader = req.headers['authorization'];
|
||||||
|
const token = authHeader && authHeader.split(' ')[1];
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({error: 'Access token required'});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = verifyToken(token);
|
||||||
|
const user = getUserById(decoded.id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(403).json({error: 'User not found'});
|
||||||
|
}
|
||||||
|
|
||||||
|
req.user = {
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
username: user.username,
|
||||||
|
display_name: user.display_name,
|
||||||
|
auth_provider: user.auth_provider
|
||||||
|
};
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(403).json({error: 'Invalid or expired token', details: error.message});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
generateToken,
|
||||||
|
verifyToken,
|
||||||
|
authenticateToken,
|
||||||
|
JWT_SECRET
|
||||||
|
};
|
||||||
63
server/db.js
63
server/db.js
@ -78,7 +78,10 @@ async function initDb() {
|
|||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all articles from the DB
|
/**
|
||||||
|
* Gets all articles from the database
|
||||||
|
* @returns {[Object]} - returns an array of all stored articles
|
||||||
|
*/
|
||||||
function getAllArticles() {
|
function getAllArticles() {
|
||||||
const result = db.exec("SELECT * FROM articles ORDER BY created_at DESC");
|
const result = db.exec("SELECT * FROM articles ORDER BY created_at DESC");
|
||||||
|
|
||||||
@ -102,7 +105,11 @@ function getAllArticles() {
|
|||||||
return articles;
|
return articles;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a specific article from the DB
|
/**
|
||||||
|
* Gets the article from the database associated with a KA number
|
||||||
|
* @param {string} ka_num - The KA number associated with an article
|
||||||
|
* @returns {Object} - the associated article from the database
|
||||||
|
*/
|
||||||
function getArticle(ka_num) {
|
function getArticle(ka_num) {
|
||||||
const stmt = db.prepare('SELECT * FROM articles where ka_number = ?');
|
const stmt = db.prepare('SELECT * FROM articles where ka_number = ?');
|
||||||
stmt.bind([ka_num]);
|
stmt.bind([ka_num]);
|
||||||
@ -117,7 +124,13 @@ function getArticle(ka_num) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new article
|
/**
|
||||||
|
* Creates a new entry in the articles table, atomatically assigns KA number, and returns the newly created article
|
||||||
|
* @param {string} title - The title of the article
|
||||||
|
* @param {string} content - The content of the article
|
||||||
|
* @param {string} author - The username of the article creator
|
||||||
|
* @returns {Object} - The newly created article from the database
|
||||||
|
*/
|
||||||
function createArticle(title, content, author) {
|
function createArticle(title, content, author) {
|
||||||
const ka_num = getNextKANumber();
|
const ka_num = getNextKANumber();
|
||||||
|
|
||||||
@ -133,6 +146,14 @@ function createArticle(title, content, author) {
|
|||||||
return getArticle(ka_num);
|
return getArticle(ka_num);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the stored article that is associated with the KA number, and returns the updated article
|
||||||
|
* @param {string} ka_num - The KA number of the article beaing updated
|
||||||
|
* @param {string} title - The title of the article
|
||||||
|
* @param {string} content - The content of the article
|
||||||
|
* @param {string} author - The username that updated the article
|
||||||
|
* @returns {Object} - The newly updatd article from the database
|
||||||
|
*/
|
||||||
function updateArticle(ka_num, title, content, author) {
|
function updateArticle(ka_num, title, content, author) {
|
||||||
db.run(
|
db.run(
|
||||||
"UPDATE articles SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP, updated_by = ? WHERE ka_number = ?",
|
"UPDATE articles SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP, updated_by = ? WHERE ka_number = ?",
|
||||||
@ -146,7 +167,10 @@ function updateArticle(ka_num, title, content, author) {
|
|||||||
return getArticle(ka_num);
|
return getArticle(ka_num);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the article from the DB
|
/**
|
||||||
|
* Deletes the associated article from the article table
|
||||||
|
* @param {*} ka_num - The KA number associated to the article
|
||||||
|
*/
|
||||||
function deleteArticle(ka_num) {
|
function deleteArticle(ka_num) {
|
||||||
db.run(
|
db.run(
|
||||||
"DELETE FROM articles WHERE ka_number = ?",
|
"DELETE FROM articles WHERE ka_number = ?",
|
||||||
@ -158,7 +182,11 @@ function deleteArticle(ka_num) {
|
|||||||
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
fs.writeFileSync(DB_PATH, Buffer.from(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple search to look for matching terms or KA number
|
/**
|
||||||
|
* Searches the article table for any words matching in the title, content, or ka number and returns an array of them
|
||||||
|
* @param {string} query - The search query that will be used for looking up the assocaited articles
|
||||||
|
* @returns {[Object]} - An arry of articles that match the query content
|
||||||
|
*/
|
||||||
function searchArticles(query) {
|
function searchArticles(query) {
|
||||||
const searchTerm = `%${query}%`;
|
const searchTerm = `%${query}%`;
|
||||||
const stmt = db.prepare(
|
const stmt = db.prepare(
|
||||||
@ -175,6 +203,16 @@ function searchArticles(query) {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new user and returns the newly created user object from the database
|
||||||
|
* @param {string} username - The username for the newly created user
|
||||||
|
* @param {string} email - The email address for the newly created user
|
||||||
|
* @param {string} passHash - The hashed password for validation purposes
|
||||||
|
* @param {string} displayName - The name that will be desplayed when an article is created or updated
|
||||||
|
* @param {string} authProvider - the source of the authentication: 'local' or 'entra'
|
||||||
|
* @param {string} entraId - The ID number for the associated entra account, can be null if auth provider is local
|
||||||
|
* @returns {Object} - The user object of the newly created user
|
||||||
|
*/
|
||||||
function createUser(username, email, passHash, displayName, authProvider = 'local', entraId = null) {
|
function createUser(username, email, passHash, displayName, authProvider = 'local', entraId = null) {
|
||||||
db.run("INSERT INTO users (username, email, pass_hash, display_name, auth_provider, entra_id) VALUES (?, ?, ?, ?, ?, ?)",
|
db.run("INSERT INTO users (username, email, pass_hash, display_name, auth_provider, entra_id) VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
[username, email, passHash, displayName, authProvider, entraId]
|
[username, email, passHash, displayName, authProvider, entraId]
|
||||||
@ -188,6 +226,11 @@ function createUser(username, email, passHash, displayName, authProvider = 'loca
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up users table and returns the user object for the user profile that matches the provided email
|
||||||
|
* @param {string} email - The email address used for the created user
|
||||||
|
* @returns {Object} - The matching user object from the database
|
||||||
|
*/
|
||||||
function getUserByEmail(email) {
|
function getUserByEmail(email) {
|
||||||
const stmt = db.prepare("SELECT * FROM users WHERE email = ? LIMIT 1");
|
const stmt = db.prepare("SELECT * FROM users WHERE email = ? LIMIT 1");
|
||||||
stmt.bind([email]);
|
stmt.bind([email]);
|
||||||
@ -202,6 +245,11 @@ function getUserByEmail(email) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up the user account with the specified username from the database and returns the user object
|
||||||
|
* @param {*} username - The username associated to the profile being looked up
|
||||||
|
* @returns {Object} - The user object that matches the provided username
|
||||||
|
*/
|
||||||
function getUserByUsername(username) {
|
function getUserByUsername(username) {
|
||||||
const stmt = db.prepare("SELECT * FROM users WHERE username = ?");
|
const stmt = db.prepare("SELECT * FROM users WHERE username = ?");
|
||||||
stmt.bind([username]);
|
stmt.bind([username]);
|
||||||
@ -216,6 +264,11 @@ function getUserByUsername(username) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looks up the user by the primary key and returns the user object
|
||||||
|
* @param {Int} id - The id number of the associated user account
|
||||||
|
* @returns {Object} - The user object associated to the primary key provided
|
||||||
|
*/
|
||||||
function getUserById(id) {
|
function getUserById(id) {
|
||||||
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
const stmt = db.prepare("SELECT * FROM users WHERE id = ?");
|
||||||
stmt.bind([id]);
|
stmt.bind([id]);
|
||||||
|
|||||||
109
server/server.js
109
server/server.js
@ -1,5 +1,18 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const { initDb, getAllArticles, getArticle, createArticle, updateArticle, deleteArticle, searchArticles } = require('./db');
|
const argon2 = require('argon2');
|
||||||
|
const {
|
||||||
|
initDb,
|
||||||
|
getAllArticles,
|
||||||
|
getArticle,
|
||||||
|
createArticle,
|
||||||
|
updateArticle,
|
||||||
|
deleteArticle,
|
||||||
|
searchArticles,
|
||||||
|
createUser,
|
||||||
|
getUserByUsername,
|
||||||
|
getUserByEmail
|
||||||
|
} = require('./db');
|
||||||
|
const { generateToken, authenticateToken } = require('./auth');
|
||||||
const app = express();
|
const app = express();
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const PORT = 9000;
|
const PORT = 9000;
|
||||||
@ -13,7 +26,7 @@ initDb().then(() => {
|
|||||||
res.json({message : 'Server is running'});
|
res.json({message : 'Server is running'});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/articles', (req, res) => {
|
app.get('/api/articles', authenticateToken, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const articles = getAllArticles();
|
const articles = getAllArticles();
|
||||||
res.json(articles);
|
res.json(articles);
|
||||||
@ -23,7 +36,7 @@ initDb().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/articles/:ka_number', (req, res) => {
|
app.get('/api/articles/:ka_number', authenticateToken, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const article = getArticle(req.params.ka_number);
|
const article = getArticle(req.params.ka_number);
|
||||||
|
|
||||||
@ -38,7 +51,7 @@ initDb().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/articles', (req, res) => {
|
app.post('/api/articles', authenticateToken, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { title, content } = req.body;
|
const { title, content } = req.body;
|
||||||
|
|
||||||
@ -46,7 +59,7 @@ initDb().then(() => {
|
|||||||
return res.status(400).json({error: 'Title is required' });
|
return res.status(400).json({error: 'Title is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const article = createArticle(title, content || '');
|
const article = createArticle(title, content, req.user.display_name);
|
||||||
res.status(201).json(article);
|
res.status(201).json(article);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -55,7 +68,7 @@ initDb().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.put('/api/articles/:ka_number', (req, res) => {
|
app.put('/api/articles/:ka_number', authenticateToken, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { title, content } = req.body;
|
const { title, content } = req.body;
|
||||||
|
|
||||||
@ -63,7 +76,7 @@ initDb().then(() => {
|
|||||||
return res.status(400).json({error: 'Title is required' });
|
return res.status(400).json({error: 'Title is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const article = updateArticle(req.params.ka_number, title, content);
|
const article = updateArticle(req.params.ka_number, title, content, req.user.display_name);
|
||||||
|
|
||||||
if (!article) {
|
if (!article) {
|
||||||
return res.status(404).json({error: 'Article not found'});
|
return res.status(404).json({error: 'Article not found'});
|
||||||
@ -76,7 +89,7 @@ initDb().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.delete('/api/articles/:ka_number', (req, res) => {
|
app.delete('/api/articles/:ka_number', authenticateToken, (req, res) => {
|
||||||
try {
|
try {
|
||||||
deleteArticle(req.params.ka_number);
|
deleteArticle(req.params.ka_number);
|
||||||
return res.status(200).json({'message': 'Successfully deleted article'});
|
return res.status(200).json({'message': 'Successfully deleted article'});
|
||||||
@ -87,7 +100,7 @@ initDb().then(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/search', (req, res) => {
|
app.get('/api/search', authenticateToken, (req, res) => {
|
||||||
try {
|
try {
|
||||||
const query = req.query.q;
|
const query = req.query.q;
|
||||||
|
|
||||||
@ -102,6 +115,84 @@ initDb().then(() => {
|
|||||||
return res.status(500).json({error: 'Failed to search articles', 'details': String(error)});
|
return res.status(500).json({error: 'Failed to search articles', 'details': String(error)});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.post('/api/auth/register', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {username, email, password, displayName} = req.body;
|
||||||
|
|
||||||
|
if (!username || !email || !password || !displayName) {
|
||||||
|
return res.status(400).json({error: 'Username, email, password, and display name are required'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cur_user_username = getUserByUsername(username);
|
||||||
|
if (cur_user_username) {
|
||||||
|
return res.status(400).json({error: 'Username already exists'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const cur_user_email = getUserByEmail(email);
|
||||||
|
if(cur_user_email) {
|
||||||
|
return res.status(400).json({error: 'email address already in use'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const pass_hash = await argon2.hash(password);
|
||||||
|
const newUser = createUser(username, email, pass_hash, displayName);
|
||||||
|
const token = generateToken(newUser);
|
||||||
|
|
||||||
|
return res.status(201).json({
|
||||||
|
user: {
|
||||||
|
id: newUser.id,
|
||||||
|
username: newUser.username,
|
||||||
|
email: newUser.email,
|
||||||
|
display_name: newUser.display_name,
|
||||||
|
auth_provider: newUser.auth_provider,
|
||||||
|
created_at: newUser.created_at
|
||||||
|
},
|
||||||
|
token
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when registering new user: ', error);
|
||||||
|
return res.status(500).json({error: 'Failed to create user', 'details': String(error)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const {username, email, password} = req.body;
|
||||||
|
|
||||||
|
if (!password || (!username && !email)) {
|
||||||
|
return res.status(400).json({error: 'Username/email & password are required'});
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
if (!username) {
|
||||||
|
user = getUserByEmail(email);
|
||||||
|
} else {
|
||||||
|
user = getUserByUsername(username);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user || !await argon2.verify(user.pass_hash, password)) {
|
||||||
|
return res.status(401).json({error: 'Invalid credentials'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = generateToken(user);
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
display_name: user.display_name,
|
||||||
|
auth_provider: user.auth_provider,
|
||||||
|
created_at: user.created_at
|
||||||
|
},
|
||||||
|
token
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to log user in:', error);
|
||||||
|
return res.status(500).json({error: 'Failed to login', 'details': String(error)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
console.log(`Server running on http://localhost:${PORT}`);
|
console.log(`Server running on http://localhost:${PORT}`);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user