Fix jwt-token
This commit is contained in:
@@ -148,6 +148,8 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
company_name: ''
|
company_name: ''
|
||||||
});
|
});
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [logoPreview, setLogoPreview] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (tenantConfig?.branding) {
|
if (tenantConfig?.branding) {
|
||||||
@@ -168,6 +170,56 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLogoUpload = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Validate file type
|
||||||
|
if (!file.type.startsWith('image/')) {
|
||||||
|
toast.error('Please select an image file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate file size (5MB)
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
toast.error('File size must be less than 5MB');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setUploading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('logo', file);
|
||||||
|
|
||||||
|
const response = await api.post('/tenant/logo-upload', formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.data.success) {
|
||||||
|
setBranding(prev => ({ ...prev, logo_url: response.data.data.logo_url }));
|
||||||
|
setLogoPreview(null);
|
||||||
|
toast.success('Logo uploaded successfully');
|
||||||
|
if (onRefresh) onRefresh();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error('Failed to upload logo');
|
||||||
|
} finally {
|
||||||
|
setUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFilePreview = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file && file.type.startsWith('image/')) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => setLogoPreview(e.target.result);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white shadow rounded-lg">
|
<div className="bg-white shadow rounded-lg">
|
||||||
<div className="px-4 py-5 sm:p-6">
|
<div className="px-4 py-5 sm:p-6">
|
||||||
@@ -184,14 +236,70 @@ const BrandingSettings = ({ tenantConfig, onRefresh }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700">Logo URL</label>
|
<label className="block text-sm font-medium text-gray-700 mb-2">Company Logo</label>
|
||||||
<input
|
|
||||||
type="url"
|
{/* Current logo display */}
|
||||||
value={branding.logo_url}
|
{branding.logo_url && (
|
||||||
onChange={(e) => setBranding(prev => ({ ...prev, logo_url: e.target.value }))}
|
<div className="mb-4">
|
||||||
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
<img
|
||||||
placeholder="https://example.com/logo.png"
|
src={branding.logo_url.startsWith('http') ? branding.logo_url : `${api.defaults.baseURL.replace('/api', '')}${branding.logo_url}`}
|
||||||
/>
|
alt="Current logo"
|
||||||
|
className="h-16 w-auto object-contain border border-gray-200 rounded p-2"
|
||||||
|
onError={(e) => {
|
||||||
|
e.target.style.display = 'none';
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Current logo</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Upload interface */}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
onChange={(e) => {
|
||||||
|
handleFilePreview(e);
|
||||||
|
handleLogoUpload(e);
|
||||||
|
}}
|
||||||
|
className="block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-medium file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100"
|
||||||
|
disabled={uploading}
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">PNG, JPG up to 5MB</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{uploading && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-primary-600"></div>
|
||||||
|
<span className="ml-2 text-sm text-gray-600">Uploading...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Preview */}
|
||||||
|
{logoPreview && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<img
|
||||||
|
src={logoPreview}
|
||||||
|
alt="Logo preview"
|
||||||
|
className="h-16 w-auto object-contain border border-gray-200 rounded p-2"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Preview</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Manual URL input as fallback */}
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700">Or enter logo URL manually</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
value={branding.logo_url}
|
||||||
|
onChange={(e) => setBranding(prev => ({ ...prev, logo_url: e.target.value }))}
|
||||||
|
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
placeholder="https://example.com/logo.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ COPY . .
|
|||||||
# Create logs directory
|
# Create logs directory
|
||||||
RUN mkdir -p logs
|
RUN mkdir -p logs
|
||||||
|
|
||||||
|
# Create uploads directory for logos
|
||||||
|
RUN mkdir -p uploads/logos
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN addgroup -g 1001 -S nodejs && \
|
RUN addgroup -g 1001 -S nodejs && \
|
||||||
adduser -S nodejs -u 1001
|
adduser -S nodejs -u 1001
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const path = require('path');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const helmet = require('helmet');
|
const helmet = require('helmet');
|
||||||
const morgan = require('morgan');
|
const morgan = require('morgan');
|
||||||
@@ -73,6 +74,9 @@ app.use(cors({
|
|||||||
app.use(express.json({ limit: '10mb' }));
|
app.use(express.json({ limit: '10mb' }));
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
// Serve uploaded files
|
||||||
|
app.use('/uploads', express.static(path.join(__dirname, 'uploads')));
|
||||||
|
|
||||||
// API Debug logging (only when API_DEBUG=true)
|
// API Debug logging (only when API_DEBUG=true)
|
||||||
if (process.env.API_DEBUG === 'true') {
|
if (process.env.API_DEBUG === 'true') {
|
||||||
console.log('🐛 API Debug logging enabled');
|
console.log('🐛 API Debug logging enabled');
|
||||||
|
|||||||
@@ -32,7 +32,8 @@
|
|||||||
"passport-openidconnect": "^0.1.1",
|
"passport-openidconnect": "^0.1.1",
|
||||||
"ldapjs": "^3.0.7",
|
"ldapjs": "^3.0.7",
|
||||||
"express-session": "^1.17.3",
|
"express-session": "^1.17.3",
|
||||||
"umzug": "^3.4.0"
|
"umzug": "^3.4.0",
|
||||||
|
"multer": "^1.4.5-lts.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1",
|
"nodemon": "^3.0.1",
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const multer = require('multer');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { Tenant, User } = require('../models');
|
const { Tenant, User } = require('../models');
|
||||||
const { authenticateToken } = require('../middleware/auth');
|
const { authenticateToken } = require('../middleware/auth');
|
||||||
@@ -13,6 +16,40 @@ const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
|||||||
// Initialize multi-tenant auth
|
// Initialize multi-tenant auth
|
||||||
const multiAuth = new MultiTenantAuth();
|
const multiAuth = new MultiTenantAuth();
|
||||||
|
|
||||||
|
// Configure multer for logo uploads
|
||||||
|
const storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
const uploadDir = path.join(__dirname, '../uploads/logos');
|
||||||
|
// Create directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(uploadDir)) {
|
||||||
|
fs.mkdirSync(uploadDir, { recursive: true });
|
||||||
|
}
|
||||||
|
cb(null, uploadDir);
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
// Use tenant ID and timestamp for unique filename
|
||||||
|
const tenantId = req.user.tenant_id;
|
||||||
|
const ext = path.extname(file.originalname);
|
||||||
|
const filename = `tenant-${tenantId}-${Date.now()}${ext}`;
|
||||||
|
cb(null, filename);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
limits: {
|
||||||
|
fileSize: 5 * 1024 * 1024 // 5MB limit
|
||||||
|
},
|
||||||
|
fileFilter: function (req, file, cb) {
|
||||||
|
// Accept only image files
|
||||||
|
if (file.mimetype.startsWith('image/')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Only image files are allowed'), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /tenant/info
|
* GET /tenant/info
|
||||||
* Get current tenant information
|
* Get current tenant information
|
||||||
@@ -71,6 +108,83 @@ router.get('/info', authenticateToken, requirePermissions(['tenant.view']), asyn
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /tenant/logo-upload
|
||||||
|
* Upload tenant logo (branding admin or higher)
|
||||||
|
*/
|
||||||
|
router.post('/logo-upload', authenticateToken, requirePermissions(['branding.edit']), upload.single('logo'), async (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.file) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'No file uploaded'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine tenant from request
|
||||||
|
const tenantId = await multiAuth.determineTenant(req);
|
||||||
|
if (!tenantId) {
|
||||||
|
return res.status(400).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Invalid tenant'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get tenant
|
||||||
|
const tenant = await Tenant.findByPk(tenantId);
|
||||||
|
if (!tenant) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Tenant not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old logo file if it exists
|
||||||
|
if (tenant.branding?.logo_url) {
|
||||||
|
const oldLogoPath = tenant.branding.logo_url.replace('/uploads/logos/', '');
|
||||||
|
const oldFilePath = path.join(__dirname, '../uploads/logos', oldLogoPath);
|
||||||
|
if (fs.existsSync(oldFilePath)) {
|
||||||
|
fs.unlinkSync(oldFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create logo URL
|
||||||
|
const logoUrl = `/uploads/logos/${req.file.filename}`;
|
||||||
|
|
||||||
|
// Update tenant branding with new logo
|
||||||
|
const updatedBranding = {
|
||||||
|
...tenant.branding,
|
||||||
|
logo_url: logoUrl
|
||||||
|
};
|
||||||
|
|
||||||
|
await tenant.update({ branding: updatedBranding });
|
||||||
|
|
||||||
|
console.log(`✅ Tenant "${tenantId}" logo uploaded by user "${req.user.username}"`);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Logo uploaded successfully',
|
||||||
|
data: {
|
||||||
|
logo_url: logoUrl,
|
||||||
|
branding: updatedBranding
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading logo:', error);
|
||||||
|
|
||||||
|
// Clean up uploaded file on error
|
||||||
|
if (req.file && fs.existsSync(req.file.path)) {
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
message: 'Failed to upload logo'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT /tenant/branding
|
* PUT /tenant/branding
|
||||||
* Update tenant branding (branding admin or higher)
|
* Update tenant branding (branding admin or higher)
|
||||||
|
|||||||
Reference in New Issue
Block a user