From e83a164be7d720099bb81884c884c3e9bede885c Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Sat, 20 Sep 2025 07:32:22 +0200 Subject: [PATCH] Fix jwt-token --- client/src/utils/tempTranslations.js | 18 +- server/tests/fixtures/test-logo.png | 1 + server/tests/routes/tenant-logo.test.js | 453 ++++++++++++++++++++++++ 3 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 server/tests/fixtures/test-logo.png create mode 100644 server/tests/routes/tenant-logo.test.js diff --git a/client/src/utils/tempTranslations.js b/client/src/utils/tempTranslations.js index ca96b4e..e667556 100644 --- a/client/src/utils/tempTranslations.js +++ b/client/src/utils/tempTranslations.js @@ -709,7 +709,23 @@ const translations = { changeLogo: 'Ändra logotyp', confirmRemoveLogo: 'Är du säker på att du vill ta bort den aktuella logotypen?', logoRemoved: 'Logotyp borttagen framgångsrikt', - logoRemoveFailed: 'Misslyckades med att ta bort logotyp' + logoRemoveFailed: 'Misslyckades med att ta bort logotyp', + // Säkerhet och behörigheter + accessDenied: 'Åtkomst nekad: Otillräckliga behörigheter', + invalidTenant: 'Ogiltig klient', + tenantNotFound: 'Klient hittades inte', + userNotMemberOfTenant: 'Åtkomst nekad: Användaren är inte medlem av klienten', + authenticationRequired: 'Autentisering krävs', + logoUploadError: 'Misslyckades med att ladda upp logotyp', + logoRemovalError: 'Misslyckades med att ta bort logotyp', + logoUploadSuccess: 'Logotyp uppladdad framgångsrikt', + logoRemovalSuccess: 'Logotyp borttagen framgångsrikt', + noFileUploaded: 'Ingen fil uppladdad', + noLogoToRemove: 'Ingen logotyp att ta bort', + internalServerError: 'Internt serverfel', + securityValidationFailed: 'Säkerhetsvalidering misslyckades', + unauthorizedAccess: 'Obehörigt åtkomstförsök', + tenantAccessDenied: 'Åtkomst nekad för denna klient' }, auth: { login: 'Logga in', diff --git a/server/tests/fixtures/test-logo.png b/server/tests/fixtures/test-logo.png new file mode 100644 index 0000000..9055871 --- /dev/null +++ b/server/tests/fixtures/test-logo.png @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg== \ No newline at end of file diff --git a/server/tests/routes/tenant-logo.test.js b/server/tests/routes/tenant-logo.test.js new file mode 100644 index 0000000..1f35c13 --- /dev/null +++ b/server/tests/routes/tenant-logo.test.js @@ -0,0 +1,453 @@ +const { describe, it, beforeEach, afterEach, before, after } = require('mocha'); +const { expect } = require('chai'); +const sinon = require('sinon'); +const request = require('supertest'); +const express = require('express'); +const fs = require('fs'); +const path = require('path'); +const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, generateTestToken } = require('../setup'); +const tenantRoutes = require('../../routes/tenant'); +const { SecurityLogger } = require('../../middleware/logger'); + +describe('Tenant Logo Management', () => { + let app, models, sequelize; + let testTenant, testUser, testUserToken; + let adminUser, adminUserToken; + let otherTenantUser, otherTenantUserToken; + let securityLoggerStub; + + before(async () => { + ({ models, sequelize } = await setupTestEnvironment()); + + // Setup express app for testing + app = express(); + app.use(express.json()); + app.use('/tenant', tenantRoutes); + + // Create uploads directory for testing + const uploadsDir = path.join(__dirname, '../../uploads/logos'); + if (!fs.existsSync(uploadsDir)) { + fs.mkdirSync(uploadsDir, { recursive: true }); + } + }); + + after(async () => { + // Clean up test uploads directory + const uploadsDir = path.join(__dirname, '../../uploads/logos'); + if (fs.existsSync(uploadsDir)) { + const files = fs.readdirSync(uploadsDir); + files.forEach(file => { + if (file.startsWith('tenant-')) { + fs.unlinkSync(path.join(uploadsDir, file)); + } + }); + } + + await teardownTestEnvironment(); + }); + + beforeEach(async () => { + await cleanDatabase(); + + // Stub SecurityLogger to capture audit logs + securityLoggerStub = sinon.stub(SecurityLogger.prototype, 'logSecurityEvent'); + + // Create test tenant + testTenant = await createTestTenant({ + slug: 'test-tenant', + name: 'Test Tenant', + branding: {} + }); + + // Create test users with different roles + testUser = await createTestUser({ + username: 'testuser', + tenant_id: testTenant.id, + role: 'tenant_user' + }); + testUserToken = generateTestToken(testUser); + + adminUser = await createTestUser({ + username: 'adminuser', + tenant_id: testTenant.id, + role: 'tenant_admin' + }); + adminUserToken = generateTestToken(adminUser); + + // Create user from different tenant + const otherTenant = await createTestTenant({ + slug: 'other-tenant', + name: 'Other Tenant' + }); + otherTenantUser = await createTestUser({ + username: 'otheruser', + tenant_id: otherTenant.id, + role: 'tenant_admin' + }); + otherTenantUserToken = generateTestToken(otherTenantUser); + }); + + afterEach(async () => { + if (securityLoggerStub) { + securityLoggerStub.restore(); + } + }); + + describe('POST /tenant/logo-upload', () => { + const testImagePath = path.join(__dirname, '../fixtures/test-logo.png'); + + beforeEach(() => { + // Create a test image file if it doesn't exist + if (!fs.existsSync(testImagePath)) { + const testImageBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64'); + fs.writeFileSync(testImagePath, testImageBuffer); + } + }); + + it('should successfully upload logo with valid permissions', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + expect(response.status).to.equal(200); + expect(response.body.success).to.be.true; + expect(response.body.message).to.equal('Logo uploaded successfully'); + expect(response.body.data.logo_url).to.include('/uploads/logos/'); + + // Verify audit log was created + expect(securityLoggerStub.calledWith('info', 'Tenant logo uploaded successfully')).to.be.true; + }); + + it('should reject upload without authentication', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .attach('logo', testImagePath); + + expect(response.status).to.equal(401); + expect(response.body.success).to.be.false; + }); + + it('should reject upload without required permissions', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${testUserToken}`) + .attach('logo', testImagePath); + + expect(response.status).to.equal(403); + expect(response.body.success).to.be.false; + }); + + it('should reject upload from user of different tenant', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${otherTenantUserToken}`) + .attach('logo', testImagePath); + + expect(response.status).to.equal(403); + expect(response.body.success).to.be.false; + expect(response.body.message).to.equal('Access denied: User not member of tenant'); + + // Verify security audit log + expect(securityLoggerStub.calledWith('warning', 'User attempted logo_upload on different tenant')).to.be.true; + }); + + it('should reject upload without file', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`); + + expect(response.status).to.equal(400); + expect(response.body.success).to.be.false; + expect(response.body.message).to.equal('No file uploaded'); + + // Verify security audit log + expect(securityLoggerStub.calledWith('warning', 'Logo upload attempted without file')).to.be.true; + }); + + it('should replace existing logo when uploading new one', async () => { + // First upload + const firstResponse = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + expect(firstResponse.status).to.equal(200); + const firstLogoUrl = firstResponse.body.data.logo_url; + + // Second upload + const secondResponse = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + expect(secondResponse.status).to.equal(200); + const secondLogoUrl = secondResponse.body.data.logo_url; + + expect(firstLogoUrl).to.not.equal(secondLogoUrl); + + // Verify old logo cleanup was logged + expect(securityLoggerStub.calledWith('info', 'Old logo file cleaned up')).to.be.true; + }); + + it('should validate tenant access and log appropriately', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + expect(response.status).to.equal(200); + + // Verify access validation log + expect(securityLoggerStub.calledWith('info', 'logo_upload access validated')).to.be.true; + }); + }); + + describe('DELETE /tenant/logo', () => { + let logoUrl; + + beforeEach(async () => { + // Upload a logo first + const testImagePath = path.join(__dirname, '../fixtures/test-logo.png'); + if (!fs.existsSync(testImagePath)) { + const testImageBuffer = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==', 'base64'); + fs.writeFileSync(testImagePath, testImageBuffer); + } + + const uploadResponse = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + logoUrl = uploadResponse.body.data.logo_url; + + // Reset stub to clear upload logs + securityLoggerStub.resetHistory(); + }); + + it('should successfully remove logo with valid permissions', async () => { + const response = await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`); + + expect(response.status).to.equal(200); + expect(response.body.success).to.be.true; + expect(response.body.message).to.equal('Logo removed successfully'); + expect(response.body.data.branding.logo_url).to.be.null; + + // Verify audit logs + expect(securityLoggerStub.calledWith('info', 'Tenant logo removed successfully')).to.be.true; + expect(securityLoggerStub.calledWith('info', 'Logo file deleted from filesystem')).to.be.true; + }); + + it('should reject removal without authentication', async () => { + const response = await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com'); + + expect(response.status).to.equal(401); + expect(response.body.success).to.be.false; + }); + + it('should reject removal without required permissions', async () => { + const response = await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${testUserToken}`); + + expect(response.status).to.equal(403); + expect(response.body.success).to.be.false; + }); + + it('should reject removal from user of different tenant', async () => { + const response = await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${otherTenantUserToken}`); + + expect(response.status).to.equal(403); + expect(response.body.success).to.be.false; + expect(response.body.message).to.equal('Access denied: User not member of tenant'); + + // Verify security audit log + expect(securityLoggerStub.calledWith('warning', 'User attempted logo_removal on different tenant')).to.be.true; + }); + + it('should handle removal when no logo exists', async () => { + // Remove the logo first + await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`); + + // Try to remove again + const response = await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`); + + expect(response.status).to.equal(400); + expect(response.body.success).to.be.false; + expect(response.body.message).to.equal('No logo to remove'); + + // Verify warning audit log + expect(securityLoggerStub.calledWith('warning', 'Logo removal attempted when no logo exists')).to.be.true; + }); + + it('should validate tenant access and log appropriately', async () => { + const response = await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`); + + expect(response.status).to.equal(200); + + // Verify access validation log + expect(securityLoggerStub.calledWith('info', 'logo_removal access validated')).to.be.true; + }); + }); + + describe('Security Validation Function', () => { + it('should log authentication failures', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com'); + + expect(response.status).to.equal(401); + + // Note: This will be logged by the authenticateToken middleware + // but we can verify the endpoint behavior + }); + + it('should log tenant validation failures', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'invalid-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`); + + expect(response.status).to.equal(400); + expect(response.body.message).to.equal('Invalid tenant'); + }); + + it('should log cross-tenant access attempts', async () => { + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${otherTenantUserToken}`); + + expect(response.status).to.equal(403); + + // Verify cross-tenant access was logged + expect(securityLoggerStub.calledWith('warning', 'User attempted logo_upload on different tenant')).to.be.true; + }); + }); + + describe('Error Handling', () => { + it('should handle file system errors during upload', async () => { + // Mock fs to throw an error + const fsStub = sinon.stub(fs, 'unlinkSync').throws(new Error('File system error')); + + try { + const testImagePath = path.join(__dirname, '../fixtures/test-logo.png'); + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + // Should still succeed despite cleanup error + expect(response.status).to.equal(200); + } finally { + fsStub.restore(); + } + }); + + it('should log errors appropriately', async () => { + // Mock models to throw an error + const modelStub = sinon.stub(models.Tenant, 'findOne').throws(new Error('Database error')); + + try { + const testImagePath = path.join(__dirname, '../fixtures/test-logo.png'); + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + expect(response.status).to.equal(500); + expect(response.body.message).to.equal('Failed to upload logo'); + + // Verify error was logged + expect(securityLoggerStub.calledWith('error', 'Logo upload failed with error')).to.be.true; + } finally { + modelStub.restore(); + } + }); + }); + + describe('Audit Trail Verification', () => { + it('should create comprehensive audit trail for logo lifecycle', async () => { + const testImagePath = path.join(__dirname, '../fixtures/test-logo.png'); + + // Upload logo + const uploadResponse = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .attach('logo', testImagePath); + + expect(uploadResponse.status).to.equal(200); + + // Remove logo + const removeResponse = await request(app) + .delete('/tenant/logo') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`); + + expect(removeResponse.status).to.equal(200); + + // Verify complete audit trail + const logCalls = securityLoggerStub.getCalls(); + const logMessages = logCalls.map(call => call.args[1]); + + expect(logMessages).to.include('logo_upload access validated'); + expect(logMessages).to.include('Tenant logo uploaded successfully'); + expect(logMessages).to.include('logo_removal access validated'); + expect(logMessages).to.include('Logo file deleted from filesystem'); + expect(logMessages).to.include('Tenant logo removed successfully'); + }); + + it('should include security context in audit logs', async () => { + const testImagePath = path.join(__dirname, '../fixtures/test-logo.png'); + + const response = await request(app) + .post('/tenant/logo-upload') + .set('Host', 'test-tenant.example.com') + .set('Authorization', `Bearer ${adminUserToken}`) + .set('User-Agent', 'Test-Agent/1.0') + .attach('logo', testImagePath); + + expect(response.status).to.equal(200); + + // Find the successful upload log + const uploadLogCall = securityLoggerStub.getCalls().find(call => + call.args[1] === 'Tenant logo uploaded successfully' + ); + + expect(uploadLogCall).to.exist; + expect(uploadLogCall.args[2]).to.include.keys(['userId', 'username', 'tenantId', 'tenantSlug', 'ip', 'userAgent']); + expect(uploadLogCall.args[2].username).to.equal('adminuser'); + expect(uploadLogCall.args[2].userAgent).to.equal('Test-Agent/1.0'); + }); + }); +}); \ No newline at end of file