268 lines
8.1 KiB
JavaScript
268 lines
8.1 KiB
JavaScript
const assert = require('assert');
|
|
const axios = require('axios');
|
|
|
|
const api = axios.create({
|
|
baseURL: 'http://127.0.0.1:3000/api',
|
|
validateStatus: () => true,
|
|
});
|
|
|
|
const SUPER_ADMIN = {
|
|
email: 'super_admin@flatlogic.com',
|
|
password: '252a3d31',
|
|
};
|
|
|
|
function decodeUserId(token) {
|
|
const payload = token.split('.')[1];
|
|
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')).user.id;
|
|
}
|
|
|
|
async function signIn(credentials) {
|
|
const response = await api.post('/auth/signin/local', credentials);
|
|
assert.strictEqual(response.status, 200, `Expected signin to succeed for ${credentials.email}, got ${response.status}: ${response.data}`);
|
|
return response.data;
|
|
}
|
|
|
|
async function sql(token, sqlQuery) {
|
|
const response = await api.post(
|
|
'/sql',
|
|
{ sql: sqlQuery },
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.strictEqual(response.status, 200, `Expected SQL helper to succeed, got ${response.status}: ${JSON.stringify(response.data)}`);
|
|
return response.data.rows;
|
|
}
|
|
|
|
async function createOrganization(token, name) {
|
|
const response = await api.post(
|
|
'/organizations',
|
|
{ data: { name } },
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.strictEqual(response.status, 200, `Expected organization creation to succeed, got ${response.status}: ${response.data}`);
|
|
|
|
const rows = await sql(token, `select id from organizations where name = '${name.replace(/'/g, "''")}' order by "createdAt" desc limit 1`);
|
|
assert.ok(rows[0]?.id, 'Expected organization lookup to return an id');
|
|
return rows[0].id;
|
|
}
|
|
|
|
async function createTenant(token, { name, subdomain, organizationsId }) {
|
|
const response = await api.post(
|
|
'/tenants',
|
|
{
|
|
data: {
|
|
name,
|
|
subdomain,
|
|
organizations: organizationsId,
|
|
default_language: 'fr',
|
|
primary_currency: 'USD',
|
|
},
|
|
},
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.strictEqual(response.status, 200, `Expected tenant creation to succeed, got ${response.status}: ${response.data}`);
|
|
}
|
|
|
|
async function promoteUser(token, { userId, email, organizationsId, roleId }) {
|
|
const response = await api.put(
|
|
`/users/${userId}`,
|
|
{
|
|
id: userId,
|
|
data: {
|
|
email,
|
|
firstName: email.split('@')[0],
|
|
emailVerified: true,
|
|
app_role: roleId,
|
|
organizations: organizationsId,
|
|
},
|
|
},
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.strictEqual(response.status, 200, `Expected user promotion to succeed, got ${response.status}: ${response.data}`);
|
|
}
|
|
|
|
async function signupTenantUser({ subdomain, organizationsId, email, roleId, superAdminToken }) {
|
|
const signupResponse = await api.post(
|
|
'/auth/signup',
|
|
{
|
|
email,
|
|
password: 'demo1234',
|
|
organizationsId,
|
|
},
|
|
{
|
|
headers: {
|
|
Host: `${subdomain}.example.com`,
|
|
Referer: `http://${subdomain}.example.com/register`,
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.strictEqual(signupResponse.status, 200, `Expected tenant signup to succeed, got ${signupResponse.status}: ${signupResponse.data}`);
|
|
|
|
const userId = decodeUserId(signupResponse.data);
|
|
await promoteUser(superAdminToken, {
|
|
userId,
|
|
email,
|
|
organizationsId,
|
|
roleId,
|
|
});
|
|
|
|
const rows = await sql(superAdminToken, `select "organizationsId" from users where id = '${userId}'`);
|
|
assert.strictEqual(rows[0].organizationsId, organizationsId, 'Expected signup to persist the resolved organizationsId');
|
|
|
|
return signIn({ email, password: 'demo1234' });
|
|
}
|
|
|
|
function buildContactPayload(suffix) {
|
|
return {
|
|
full_name: 'Jean Test',
|
|
company_name: `Company ${suffix}`,
|
|
email: `lead-${suffix}@company.com`,
|
|
phone: '+243800000000',
|
|
service_line: 'cloud',
|
|
company_size: '11_50',
|
|
budget_range: '20_50k',
|
|
preferred_channel: 'email',
|
|
urgency: 'urgent',
|
|
message: 'Nous avons besoin d\'une migration cloud sécurisée et bien cadrée.',
|
|
};
|
|
}
|
|
|
|
describe('tenant isolation and tenant-scoped public intake', function () {
|
|
this.timeout(120000);
|
|
|
|
it('rejects tenant discovery and contact intake when no valid tenant context is present', async () => {
|
|
const orgResponse = await api.get('/org-for-auth');
|
|
assert.strictEqual(orgResponse.status, 400);
|
|
assert.match(String(orgResponse.data), /tenant subdomain/i);
|
|
|
|
const intakeResponse = await api.post('/contact-form', buildContactPayload('unknown'), {
|
|
headers: {
|
|
Host: 'unknown.example.com',
|
|
Referer: 'http://unknown.example.com/',
|
|
},
|
|
});
|
|
assert.strictEqual(intakeResponse.status, 400);
|
|
assert.match(String(intakeResponse.data), /No tenant is configured/i);
|
|
});
|
|
|
|
it('prevents cross-organization read, update, delete, and direct SQL access by id', async () => {
|
|
const superAdminToken = await signIn(SUPER_ADMIN);
|
|
const timestamp = Date.now();
|
|
const adminRoleRows = await sql(superAdminToken, "select id from roles where name = 'Administrator' limit 1");
|
|
const adminRoleId = adminRoleRows[0].id;
|
|
|
|
const orgA = await createOrganization(superAdminToken, `Isolation Org A ${timestamp}`);
|
|
const orgB = await createOrganization(superAdminToken, `Isolation Org B ${timestamp}`);
|
|
const subA = `isolation-a-${timestamp}`;
|
|
const subB = `isolation-b-${timestamp}`;
|
|
|
|
await createTenant(superAdminToken, {
|
|
name: `Isolation Tenant A ${timestamp}`,
|
|
subdomain: subA,
|
|
organizationsId: orgA,
|
|
});
|
|
await createTenant(superAdminToken, {
|
|
name: `Isolation Tenant B ${timestamp}`,
|
|
subdomain: subB,
|
|
organizationsId: orgB,
|
|
});
|
|
|
|
const tenantOrgResponse = await api.get('/org-for-auth', {
|
|
headers: {
|
|
Host: `${subA}.example.com`,
|
|
},
|
|
});
|
|
assert.strictEqual(tenantOrgResponse.status, 200);
|
|
assert.strictEqual(tenantOrgResponse.data[0].id, orgA);
|
|
|
|
const userAToken = await signupTenantUser({
|
|
subdomain: subA,
|
|
organizationsId: orgA,
|
|
email: `tenant-a-${timestamp}@example.com`,
|
|
roleId: adminRoleId,
|
|
superAdminToken,
|
|
});
|
|
const userBToken = await signupTenantUser({
|
|
subdomain: subB,
|
|
organizationsId: orgB,
|
|
email: `tenant-b-${timestamp}@example.com`,
|
|
roleId: adminRoleId,
|
|
superAdminToken,
|
|
});
|
|
|
|
const intakeResponse = await api.post('/contact-form', buildContactPayload(timestamp), {
|
|
headers: {
|
|
Host: `${subA}.example.com`,
|
|
Referer: `http://${subA}.example.com/`,
|
|
},
|
|
});
|
|
assert.strictEqual(intakeResponse.status, 200, JSON.stringify(intakeResponse.data));
|
|
const { leadId } = intakeResponse.data;
|
|
assert.ok(leadId, 'Expected contact form to create a lead');
|
|
|
|
const readOwnLead = await api.get(`/leads/${leadId}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${userAToken}`,
|
|
},
|
|
});
|
|
assert.strictEqual(readOwnLead.status, 200);
|
|
assert.strictEqual(readOwnLead.data.id, leadId);
|
|
|
|
const readOtherLead = await api.get(`/leads/${leadId}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${userBToken}`,
|
|
},
|
|
});
|
|
assert.strictEqual(readOtherLead.status, 404);
|
|
|
|
const updateOtherLead = await api.put(
|
|
`/leads/${leadId}`,
|
|
{
|
|
id: leadId,
|
|
data: {
|
|
status: 'new',
|
|
},
|
|
},
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${userBToken}`,
|
|
},
|
|
},
|
|
);
|
|
assert.ok([400, 404].includes(updateOtherLead.status), `Expected cross-org update to fail, got ${updateOtherLead.status}`);
|
|
|
|
const deleteOtherLead = await api.delete(`/leads/${leadId}`, {
|
|
headers: {
|
|
Authorization: `Bearer ${userBToken}`,
|
|
},
|
|
});
|
|
assert.strictEqual(deleteOtherLead.status, 404);
|
|
|
|
const visibleRowsForOwner = await sql(userAToken, `select id from leads where id = '${leadId}'`);
|
|
assert.strictEqual(visibleRowsForOwner.length, 1, 'Expected owner SQL query to return the lead');
|
|
|
|
const visibleRowsForOtherOrg = await sql(userBToken, `select id from leads where id = '${leadId}'`);
|
|
assert.strictEqual(visibleRowsForOtherOrg.length, 0, 'Expected RLS to hide the lead from another organization');
|
|
});
|
|
});
|