prepped files for container deployment

This commit is contained in:
MattLeo 2025-12-09 08:17:44 -06:00
parent f90435399d
commit a991d18685
12 changed files with 102 additions and 28 deletions

View File

@ -65,7 +65,7 @@ function App() {
}; };
const fetchArticles = () => { const fetchArticles = () => {
fetch('http://localhost:9000/api/articles', { fetch('/api/articles', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
}, },
@ -97,7 +97,7 @@ function App() {
fetchArticles(); fetchArticles();
} }
fetch(`http://localhost:9000/api/search/advanced?${params.toString()}`, { fetch(`/api/search/advanced?${params.toString()}`, {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@ -130,7 +130,7 @@ function App() {
const isNew = !updatedArticle.ka_number || updatedArticle.ka_number === 'New Article'; const isNew = !updatedArticle.ka_number || updatedArticle.ka_number === 'New Article';
if (isNew) { if (isNew) {
fetch('http://localhost:9000/api/articles', { fetch('/api/articles', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -146,7 +146,7 @@ function App() {
}) })
.catch(error => console.error('Error creating article:', error)); .catch(error => console.error('Error creating article:', error));
} else { } else {
fetch(`http://localhost:9000/api/articles/${updatedArticle.ka_number}`, { fetch(`/api/articles/${updatedArticle.ka_number}`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -176,7 +176,7 @@ function App() {
const handleCreateNew = async () => { const handleCreateNew = async () => {
try { try {
const response = await fetch('http://localhost:9000/api/articles', { const response = await fetch('/api/articles', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
@ -205,7 +205,7 @@ function App() {
const handleDelete = () => { const handleDelete = () => {
if (confirm('Are you sure you want to delete this article?')) { if (confirm('Are you sure you want to delete this article?')) {
fetch(`http://localhost:9000/api/articles/${selectedArticle.ka_number}`, { fetch(`/api/articles/${selectedArticle.ka_number}`, {
method: 'DELETE', method: 'DELETE',
headers: { 'Authorization': `Bearer ${token}` } headers: { 'Authorization': `Bearer ${token}` }
}) })

View File

@ -14,7 +14,7 @@ function AdminPanel({ token, onBack }) {
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const response = await fetch('http://localhost:9000/api/admin/users', { const response = await fetch('/api/admin/users', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@ -41,7 +41,7 @@ function AdminPanel({ token, onBack }) {
const handleSaveRole = async (userId) => { const handleSaveRole = async (userId) => {
try { try {
const response = await fetch(`http://localhost:9000/api/admin/users/${userId}/role`, { const response = await fetch(`/api/admin/users/${userId}/role`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
@ -76,7 +76,7 @@ function AdminPanel({ token, onBack }) {
} }
try { try {
const response = await fetch(`http://localhost:9000/api/admin/users/${userId}`, { const response = await fetch(`/api/admin/users/${userId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`

View File

@ -125,7 +125,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
const response = await fetch('http://localhost:9000/api/categories', { const response = await fetch('/api/categories', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@ -142,7 +142,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
const fetchTags = async () => { const fetchTags = async () => {
try { try {
const response = await fetch('http://localhost:9000/api/tags', { const response = await fetch('/api/tags', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@ -171,7 +171,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
onSave(articleData); onSave(articleData);
try { try {
await fetch(`http://localhost:9000/api/articles/${article.ka_number}/categories`, { await fetch(`/api/articles/${article.ka_number}/categories`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
@ -180,7 +180,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
body: JSON.stringify({ categories: selectedCategories }) body: JSON.stringify({ categories: selectedCategories })
}); });
await fetch(`http://localhost:9000/api/articles/${article.ka_number}/tags`, { await fetch(`/api/articles/${article.ka_number}/tags`, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,
@ -222,7 +222,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
if (!quill) return; if (!quill) return;
const range = quill.getSelection(true); const range = quill.getSelection(true);
const mediaUrl = `http://localhost:9000${mediaItem.url}`; const mediaUrl = `/${mediaItem.url}`;
if (mediaItem.type === 'image') { if (mediaItem.type === 'image') {
quill.insertEmbed(range.index, 'image', mediaUrl); quill.insertEmbed(range.index, 'image', mediaUrl);
@ -241,7 +241,7 @@ function ArticleEditor({ article, onSave, onCancel, token }) {
if (!newCategoryInput.trim()) return; if (!newCategoryInput.trim()) return;
try { try {
const response = await fetch('http://localhost:9000/api/categories', { const response = await fetch('/api/categories', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}`, 'Authorization': `Bearer ${token}`,

View File

@ -12,7 +12,7 @@ function DraftsList({ token, onBack, onSelectDraft }) {
const fetchDrafts = async () => { const fetchDrafts = async () => {
try { try {
const response = await fetch('http://localhost:9000/api/articles/drafts', { const response = await fetch('\/api/articles/drafts', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@ -38,7 +38,7 @@ function DraftsList({ token, onBack, onSelectDraft }) {
if(!confirm('Are you sure you want to delete this draft?')) return; if(!confirm('Are you sure you want to delete this draft?')) return;
try { try {
const response = await fetch(`http://localhost:9000/api/articles/${kaNumber}`, { const response = await fetch(`/api/articles/${kaNumber}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`

View File

@ -30,7 +30,7 @@ function FilterBar({ token, onFilterChange }) {
const fetchCategories = async () => { const fetchCategories = async () => {
try { try {
const response = await fetch('http://localhost:9000/api/categories', { const response = await fetch('/api/categories', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }
@ -47,7 +47,7 @@ function FilterBar({ token, onFilterChange }) {
const fetchTags = async () => { const fetchTags = async () => {
try { try {
const response = await fetch('http://localhost:9000/api/tags', { const response = await fetch('/api/tags', {
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
} }

View File

@ -35,7 +35,7 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
setLoading(true); setLoading(true);
try { try {
const response = await fetch('http://localhost:9000/api/auth/login', { const response = await fetch('/api/auth/login', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -76,7 +76,7 @@ function Login({ onLoginSuccess, onSwitchToRegister }) {
const loginResponse = await msalInstance.loginPopup(loginRequest); const loginResponse = await msalInstance.loginPopup(loginRequest);
// Send the access token to your backend // Send the access token to your backend
const response = await fetch('http://localhost:9000/api/auth/microsoft', { const response = await fetch('/api/auth/microsoft', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

View File

@ -44,7 +44,7 @@ function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
formData.append('file', file); formData.append('file', file);
try { try {
const response = await fetch(`http://localhost:9000/api/articles/${kaNumber}/media`, { const response = await fetch(`/api/articles/${kaNumber}/media`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
@ -72,7 +72,7 @@ function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
if (!confirm('Are you sure you want to delete this file?')) return; if (!confirm('Are you sure you want to delete this file?')) return;
try { try {
const response = await fetch(`http://localhost:9000/api/articles/${kaNumber}/media/${filename}`, { const response = await fetch(`/api/articles/${kaNumber}/media/${filename}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`
@ -139,7 +139,7 @@ function MediaGallery({ kaNumber, token, onInsertMedia, isOpen, onToggle }) {
<div key={item.filename} className='media-item'> <div key={item.filename} className='media-item'>
{item.type === 'image' ? ( {item.type === 'image' ? (
<img <img
src={`http://localhost:9000${item.url}`} src={`${item.url}`}
alt={item.filename} alt={item.filename}
onClick={() => onInsertMedia(item)} onClick={() => onInsertMedia(item)}
/> />

View File

@ -27,7 +27,7 @@ function Registration({ onRegistrationSuccess, onSwitchToLogin }) {
setLoading(true); setLoading(true);
try { try {
const response = await fetch('http://localhost:9000/api/auth/register', { const response = await fetch('/api/auth/register', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'

39
docker-compose.yml Normal file
View File

@ -0,0 +1,39 @@
version: '3.8'
services:
knowledge-base:
build:
context: .
dockerfile: Dockerfile
container_name: knowledge-base
restart: unless-stopped
environment:
- NODE_ENV=production
- PORT=9000
- JWT_SECRET=${JWT_SECRET}
- MICROSOFT_CLIENT_ID=${MICROSOFT_CLIENT_ID}
- MICROSOFT_CLIENT_SECRET=${MICROSOFT_CLIENT_SECRET}
- MICROSOFT_TENANT_ID=${MICROSOFT_TENANT_ID}
volumes:
- kb-data:/app/kb.db
- kb-media:/app/media
networks:
- traefik
labels:
# Traefik routing
- "traefik.enable=true"
- "traefik.http.routers.kb.rule=Host(`kb.jv.com`)"
- "traefik.http.routers.kb.entrypoints=websecure"
- "traefik.http.routers.kb.tls=true"
- "traefik.http.services.kb.loadbalancer.server.port=9000"
- "traefik.docker.network=traefik_traefik-proxy"
networks:
traefik:
external: true
volumes:
kb-data:
driver: local
kb-media:
driver: local

23
dockerfile Normal file
View File

@ -0,0 +1,23 @@
# Combined Frontend + Backend Dockerfile
# Multi-stage build
FROM node:20-slim AS frontend-build
WORKDIR /app/frontend
COPY client/kb-frontend/package*.json ./
RUN npm ci
COPY client/kb-frontend ./
RUN npm run build
FROM node:20-slim
RUN apt-get update && apt-get install -y \
python3 \
make \
g++ \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY server/package*.json ./
RUN npm ci --only=production
COPY server/ ./
COPY --from=frontend-build /app/frontend/dist ./frontend/dist
RUN mkdir -p /app/media
EXPOSE 9000
CMD ["node", "server.js"]

View File

@ -1,7 +1,7 @@
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const {getUserById} = require('./db'); const {getUserById} = require('./db');
const JWT_SECRET = 'THISISTOTALLYSECRETDONTLOOK'; const JWT_SECRET = 'T0JcuWfi3A2GdRd71FnBmH2c16guYVT/d3Jo1I9hdcs=';
const JWT_EXPIRATION = '24h'; const JWT_EXPIRATION = '24h';

View File

@ -39,7 +39,7 @@ const PORT = 9000;
app.use(cors()) app.use(cors())
app.use(express.json()); app.use(express.json());
const msalClient = new msal.ConfidentialClientApplication(entraConfig);
const upload = multer({ const upload = multer({
dest: './temp_uploads/', dest: './temp_uploads/',
limits: { limits: {
@ -56,6 +56,12 @@ if (!fs.existsSync('./media')) {
initDb().then(() => { initDb().then(() => {
app.use('/api/auth', authRoutes);
app.use('/api/articles', articleRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/categories', categoryRoutes);
app.use('/api/tags', tagRoutes);
app.get('/', (req, res) => { app.get('/', (req, res) => {
res.json({ message: 'Server is running' }); res.json({ message: 'Server is running' });
}); });
@ -666,9 +672,15 @@ initDb().then(() => {
} }
}); });
app.use(express.static(path.join(__dirname, 'frontend/dist')));
app.use('/media', express.static(path.join(__dirname, 'media')));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'frontend/dist', 'index.html'));
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`); console.log(`Server listening on ${PORT}`);
}); });
});
}).catch(err => { }).catch(err => {
console.error('Failed to initialize database:', err); console.error('Failed to initialize database:', err);
}); });