Fix jwt-token
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Migration: Add tenant support to devices table
|
||||
* This migration adds tenant_id field to devices for multi-tenant isolation
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
// Check if tenant_id column already exists
|
||||
const tableDescription = await queryInterface.describeTable('devices');
|
||||
|
||||
if (!tableDescription.tenant_id) {
|
||||
// Add tenant_id column to devices table
|
||||
await queryInterface.addColumn('devices', 'tenant_id', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true, // Nullable for backward compatibility
|
||||
references: {
|
||||
model: 'tenants',
|
||||
key: 'id'
|
||||
},
|
||||
onUpdate: 'CASCADE',
|
||||
onDelete: 'SET NULL',
|
||||
comment: 'Foreign key to tenants table for multi-tenant isolation'
|
||||
});
|
||||
|
||||
// Add index for tenant_id for better query performance
|
||||
try {
|
||||
await queryInterface.addIndex('devices', ['tenant_id'], {
|
||||
name: 'devices_tenant_id_idx'
|
||||
});
|
||||
console.log('✅ Added index on devices.tenant_id');
|
||||
} catch (error) {
|
||||
if (error.parent?.code === '42P07') { // Index already exists
|
||||
console.log('⚠️ Index devices_tenant_id already exists, skipping...');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Associate existing devices with default tenant (backward compatibility)
|
||||
const defaultTenant = await queryInterface.sequelize.query(
|
||||
'SELECT id FROM tenants WHERE slug = :slug',
|
||||
{
|
||||
replacements: { slug: 'default' },
|
||||
type: Sequelize.QueryTypes.SELECT
|
||||
}
|
||||
);
|
||||
|
||||
if (defaultTenant.length > 0) {
|
||||
await queryInterface.sequelize.query(
|
||||
'UPDATE devices SET tenant_id = :tenantId WHERE tenant_id IS NULL',
|
||||
{
|
||||
replacements: { tenantId: defaultTenant[0].id },
|
||||
type: Sequelize.QueryTypes.UPDATE
|
||||
}
|
||||
);
|
||||
console.log('✅ Associated existing devices with default tenant');
|
||||
}
|
||||
|
||||
console.log('✅ Added tenant_id field to devices table');
|
||||
} else {
|
||||
console.log('⚠️ Column tenant_id already exists in devices table, skipping...');
|
||||
}
|
||||
},
|
||||
|
||||
async down(queryInterface, Sequelize) {
|
||||
// Remove index
|
||||
try {
|
||||
await queryInterface.removeIndex('devices', 'devices_tenant_id_idx');
|
||||
} catch (error) {
|
||||
console.log('⚠️ Index devices_tenant_id_idx does not exist, skipping...');
|
||||
}
|
||||
|
||||
// Remove column
|
||||
await queryInterface.removeColumn('devices', 'tenant_id');
|
||||
console.log('✅ Removed tenant_id field from devices table');
|
||||
}
|
||||
};
|
||||
@@ -39,6 +39,15 @@ module.exports = (sequelize) => {
|
||||
defaultValue: false,
|
||||
comment: 'Whether the device is approved to send data'
|
||||
},
|
||||
tenant_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true,
|
||||
references: {
|
||||
model: 'tenants',
|
||||
key: 'id'
|
||||
},
|
||||
comment: 'Foreign key to tenants table for multi-tenant isolation'
|
||||
},
|
||||
last_heartbeat: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
|
||||
@@ -49,6 +49,9 @@ AlertLog.belongsTo(DroneDetection, { foreignKey: 'detection_id', as: 'detection'
|
||||
Tenant.hasMany(User, { foreignKey: 'tenant_id', as: 'users' });
|
||||
User.belongsTo(Tenant, { foreignKey: 'tenant_id', as: 'tenant' });
|
||||
|
||||
Tenant.hasMany(Device, { foreignKey: 'tenant_id', as: 'devices' });
|
||||
Device.belongsTo(Tenant, { foreignKey: 'tenant_id', as: 'tenant' });
|
||||
|
||||
module.exports = {
|
||||
sequelize,
|
||||
Device,
|
||||
|
||||
@@ -125,45 +125,15 @@ async function handleHeartbeat(req, res) {
|
||||
let device = await Device.findOne({ where: { id: deviceId } });
|
||||
|
||||
if (!device) {
|
||||
// Create new device as unapproved with coordinates if provided
|
||||
const deviceData = {
|
||||
id: deviceId,
|
||||
name: `Device ${deviceId}`,
|
||||
last_heartbeat: new Date(),
|
||||
is_approved: false
|
||||
};
|
||||
// Device not found - reject heartbeat and require manual registration
|
||||
console.log(`<EFBFBD> Heartbeat rejected from unknown device ${deviceId} - device must be manually registered first`);
|
||||
|
||||
// Add coordinates if provided in heartbeat
|
||||
if (geo_lat && geo_lon) {
|
||||
deviceData.geo_lat = geo_lat;
|
||||
deviceData.geo_lon = geo_lon;
|
||||
console.log(`📍 Setting device coordinates: ${geo_lat}, ${geo_lon}`);
|
||||
}
|
||||
|
||||
// Add location description if provided
|
||||
if (location_description) {
|
||||
deviceData.location_description = location_description;
|
||||
console.log(`📍 Setting device location: ${location_description}`);
|
||||
}
|
||||
|
||||
device = await Device.create(deviceData);
|
||||
|
||||
// Emit notification for new device requiring approval
|
||||
req.io.emit('new_device_pending', {
|
||||
device_id: deviceId,
|
||||
device_key: key,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: `New device ${deviceId} (${key}) requires approval`
|
||||
});
|
||||
|
||||
console.log(`⚠️ New unapproved device ${deviceId} created, awaiting approval`);
|
||||
|
||||
return res.status(202).json({
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Device not approved',
|
||||
message: 'Device has been registered but requires approval before it can send data',
|
||||
error: 'Device not registered',
|
||||
message: 'Device not found. Please register the device manually through the UI before sending data.',
|
||||
device_id: deviceId,
|
||||
approval_required: true
|
||||
registration_required: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,32 +213,15 @@ async function handleDetection(req, res) {
|
||||
let device = await Device.findOne({ where: { id: detectionData.device_id } });
|
||||
|
||||
if (!device) {
|
||||
// Create new device as unapproved
|
||||
device = await Device.create({
|
||||
id: detectionData.device_id,
|
||||
name: `Device ${detectionData.device_id}`,
|
||||
geo_lat: detectionData.geo_lat || 0,
|
||||
geo_lon: detectionData.geo_lon || 0,
|
||||
last_heartbeat: new Date(),
|
||||
is_approved: false,
|
||||
is_active: false
|
||||
});
|
||||
// Device not found - reject detection and require manual registration
|
||||
console.log(`🚫 Detection rejected from unknown device ${detectionData.device_id} - device must be manually registered first`);
|
||||
|
||||
// Emit notification for new device requiring approval
|
||||
req.io.emit('new_device_pending', {
|
||||
device_id: detectionData.device_id,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: `New device ${detectionData.device_id} requires approval`
|
||||
});
|
||||
|
||||
console.log(`⚠️ New unapproved device ${detectionData.device_id} created, awaiting approval`);
|
||||
|
||||
return res.status(202).json({
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
error: 'Device not approved',
|
||||
message: 'Device has been registered but requires approval before it can send data',
|
||||
error: 'Device not registered',
|
||||
message: 'Device not found. Please register the device manually through the UI before sending data.',
|
||||
device_id: detectionData.device_id,
|
||||
approval_required: true
|
||||
registration_required: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const Joi = require('joi');
|
||||
const { Device, DroneDetection, Heartbeat } = require('../models');
|
||||
const { Device, DroneDetection, Heartbeat, Tenant } = require('../models');
|
||||
const { validateRequest } = require('../middleware/validation');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
const MultiTenantAuth = require('../middleware/multi-tenant-auth');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
// Initialize multi-tenant auth
|
||||
const multiAuth = new MultiTenantAuth();
|
||||
|
||||
// Validation schema for device
|
||||
const deviceSchema = Joi.object({
|
||||
id: Joi.number().integer().required(),
|
||||
id: Joi.number().integer().required().min(1).max(999999999), // Device ID is required for manual registration
|
||||
name: Joi.string().max(255).allow('').optional(),
|
||||
geo_lat: Joi.number().min(-90).max(90).optional(),
|
||||
geo_lon: Joi.number().min(-180).max(180).optional(),
|
||||
@@ -34,6 +38,23 @@ const updateDeviceSchema = Joi.object({
|
||||
// GET /api/devices - Get all devices
|
||||
router.get('/', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
// Determine tenant from request
|
||||
const tenantId = await multiAuth.determineTenant(req);
|
||||
if (!tenantId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Unable to determine tenant'
|
||||
});
|
||||
}
|
||||
|
||||
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||
if (!tenant) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tenant not found'
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
include_stats = false,
|
||||
active_only = false,
|
||||
@@ -41,7 +62,7 @@ router.get('/', authenticateToken, async (req, res) => {
|
||||
offset = 0
|
||||
} = req.query;
|
||||
|
||||
const whereClause = {};
|
||||
const whereClause = { tenant_id: tenant.id };
|
||||
if (active_only === 'true') {
|
||||
whereClause.is_active = true;
|
||||
}
|
||||
@@ -201,7 +222,28 @@ router.get('/map', authenticateToken, async (req, res) => {
|
||||
// GET /api/devices/:id - Get specific device
|
||||
router.get('/:id', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const device = await Device.findByPk(req.params.id, {
|
||||
// Determine tenant from request
|
||||
const tenantId = await multiAuth.determineTenant(req);
|
||||
if (!tenantId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Unable to determine tenant'
|
||||
});
|
||||
}
|
||||
|
||||
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||
if (!tenant) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tenant not found'
|
||||
});
|
||||
}
|
||||
|
||||
const device = await Device.findOne({
|
||||
where: {
|
||||
id: req.params.id,
|
||||
tenant_id: tenant.id
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Heartbeat,
|
||||
@@ -221,7 +263,7 @@ router.get('/:id', authenticateToken, async (req, res) => {
|
||||
if (!device) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Device not found'
|
||||
message: 'Device not found in your tenant'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -243,7 +285,49 @@ router.get('/:id', authenticateToken, async (req, res) => {
|
||||
// POST /api/devices - Create new device (admin only)
|
||||
router.post('/', authenticateToken, validateRequest(deviceSchema), async (req, res) => {
|
||||
try {
|
||||
const device = await Device.create(req.body);
|
||||
// Determine tenant from request
|
||||
const tenantId = await multiAuth.determineTenant(req);
|
||||
if (!tenantId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Unable to determine tenant'
|
||||
});
|
||||
}
|
||||
|
||||
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||
if (!tenant) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tenant not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if device ID already exists in this tenant
|
||||
const existingDevice = await Device.findOne({
|
||||
where: {
|
||||
id: req.body.id,
|
||||
tenant_id: tenant.id
|
||||
}
|
||||
});
|
||||
|
||||
if (existingDevice) {
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: 'Device with this ID already exists in your tenant'
|
||||
});
|
||||
}
|
||||
|
||||
// Create device with tenant association
|
||||
const deviceData = {
|
||||
...req.body,
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true, // Manually created devices are automatically approved
|
||||
is_active: true
|
||||
};
|
||||
|
||||
const device = await Device.create(deviceData);
|
||||
|
||||
console.log(`✅ Device ${device.id} created in tenant "${tenantId}" by user "${req.user.username}"`);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
|
||||
@@ -564,6 +564,115 @@ router.put('/users/:userId/status', authenticateToken, requirePermissions(['user
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* PUT /tenant/users/:userId
|
||||
* Update user details (user admin or higher, local auth only)
|
||||
*/
|
||||
router.put('/users/:userId', authenticateToken, requirePermissions(['users.edit']), async (req, res) => {
|
||||
try {
|
||||
// Determine tenant from request
|
||||
const tenantId = await multiAuth.determineTenant(req);
|
||||
if (!tenantId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: 'Unable to determine tenant'
|
||||
});
|
||||
}
|
||||
|
||||
const tenant = await Tenant.findOne({ where: { slug: tenantId } });
|
||||
if (!tenant) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'Tenant not found'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if tenant uses local authentication
|
||||
if (tenant.auth_provider !== 'local') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: `User management is only available for local authentication. This tenant uses ${tenant.auth_provider}.`
|
||||
});
|
||||
}
|
||||
|
||||
const { userId } = req.params;
|
||||
const { email, first_name, last_name, phone, role, password } = req.body;
|
||||
|
||||
// Find user in this tenant
|
||||
const user = await User.findOne({
|
||||
where: {
|
||||
id: userId,
|
||||
tenant_id: tenant.id
|
||||
}
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: 'User not found in this tenant'
|
||||
});
|
||||
}
|
||||
|
||||
// Prepare update data
|
||||
const updateData = {};
|
||||
|
||||
if (email !== undefined) updateData.email = email;
|
||||
if (first_name !== undefined) updateData.first_name = first_name;
|
||||
if (last_name !== undefined) updateData.last_name = last_name;
|
||||
if (phone !== undefined) updateData.phone = phone;
|
||||
|
||||
// Role update with permission check
|
||||
if (role !== undefined) {
|
||||
// Check if current user has permission to assign this role
|
||||
if (!hasPermission(req.user.role, 'users.edit')) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: 'Insufficient permissions to change user roles'
|
||||
});
|
||||
}
|
||||
updateData.role = role;
|
||||
}
|
||||
|
||||
// Password update
|
||||
if (password && password.trim()) {
|
||||
const bcrypt = require('bcryptjs');
|
||||
updateData.password_hash = await bcrypt.hash(password.trim(), 10);
|
||||
}
|
||||
|
||||
// Update user
|
||||
await user.update(updateData);
|
||||
|
||||
console.log(`✅ User "${user.username}" updated in tenant "${tenantId}" by admin "${req.user.username}"`);
|
||||
|
||||
// Return updated user data (without password)
|
||||
const userData = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
first_name: user.first_name,
|
||||
last_name: user.last_name,
|
||||
phone: user.phone,
|
||||
role: user.role,
|
||||
is_active: user.is_active,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'User updated successfully',
|
||||
data: userData
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating user:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: 'Failed to update user'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /tenant/auth
|
||||
* Get authentication configuration (auth admins or higher)
|
||||
|
||||
Reference in New Issue
Block a user