Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
185
server/migrations/20251208000000-add-rbac.js
Normal file
185
server/migrations/20251208000000-add-rbac.js
Normal file
@@ -0,0 +1,185 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
const ROLES = ['admin', 'ui_team', 'ux_team', 'qa_team'];
|
||||
const ACTIONS = ['create', 'read', 'update', 'delete'];
|
||||
|
||||
export default {
|
||||
up: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
// 1. Add role and team_id columns to Users table
|
||||
await queryInterface.addColumn('Users', 'team_id', {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true
|
||||
}, { transaction });
|
||||
|
||||
// Update existing users to use new role enum values
|
||||
// First, add a temporary column
|
||||
await queryInterface.addColumn('Users', 'role_new', {
|
||||
type: DataTypes.ENUM(...ROLES),
|
||||
allowNull: true
|
||||
}, { transaction });
|
||||
|
||||
// Map old roles to new roles (if Users table exists with old roles)
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "Users" SET role_new = CASE
|
||||
WHEN role = 'admin' THEN 'admin'
|
||||
WHEN role = 'designer' THEN 'ux_team'
|
||||
WHEN role = 'developer' THEN 'ui_team'
|
||||
WHEN role = 'viewer' THEN 'qa_team'
|
||||
ELSE 'ui_team'
|
||||
END
|
||||
`, { transaction });
|
||||
|
||||
// Drop old role column
|
||||
await queryInterface.removeColumn('Users', 'role', { transaction });
|
||||
|
||||
// Rename new role column to role
|
||||
await queryInterface.renameColumn('Users', 'role_new', 'role', { transaction });
|
||||
|
||||
// Set default value for role
|
||||
await queryInterface.changeColumn('Users', 'role', {
|
||||
type: DataTypes.ENUM(...ROLES),
|
||||
allowNull: false,
|
||||
defaultValue: 'ui_team'
|
||||
}, { transaction });
|
||||
|
||||
// 2. Create Team Permissions table
|
||||
await queryInterface.createTable('TeamPermissions', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
role: {
|
||||
type: DataTypes.ENUM(...ROLES),
|
||||
allowNull: false
|
||||
},
|
||||
permission: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
resource: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.ENUM(...ACTIONS),
|
||||
allowNull: false
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
}
|
||||
}, { transaction });
|
||||
|
||||
// 3. Add Unique Constraint
|
||||
await queryInterface.addConstraint('TeamPermissions', {
|
||||
fields: ['role', 'resource', 'action'],
|
||||
type: 'unique',
|
||||
name: 'unique_role_resource_action',
|
||||
transaction
|
||||
});
|
||||
|
||||
// 4. Seed Default Permissions
|
||||
const timestamp = new Date();
|
||||
const seeds = [
|
||||
// UI Team Permissions
|
||||
{ role: 'ui_team', permission: 'sync_figma', resource: 'figma', action: 'create' },
|
||||
{ role: 'ui_team', permission: 'view_figma', resource: 'figma', action: 'read' },
|
||||
{ role: 'ui_team', permission: 'quickwins', resource: 'analysis', action: 'read' },
|
||||
{ role: 'ui_team', permission: 'regression', resource: 'analysis', action: 'create' },
|
||||
{ role: 'ui_team', permission: 'view_metrics', resource: 'metrics', action: 'read' },
|
||||
|
||||
// UX Team Permissions
|
||||
{ role: 'ux_team', permission: 'view_components', resource: 'components', action: 'read' },
|
||||
{ role: 'ux_team', permission: 'update_components', resource: 'components', action: 'update' },
|
||||
{ role: 'ux_team', permission: 'view_tokens', resource: 'tokens', action: 'read' },
|
||||
{ role: 'ux_team', permission: 'update_tokens', resource: 'tokens', action: 'update' },
|
||||
{ role: 'ux_team', permission: 'view_icons', resource: 'icons', action: 'read' },
|
||||
{ role: 'ux_team', permission: 'update_icons', resource: 'icons', action: 'update' },
|
||||
{ role: 'ux_team', permission: 'customize_figma_plugin', resource: 'figma', action: 'update' },
|
||||
{ role: 'ux_team', permission: 'view_metrics', resource: 'metrics', action: 'read' },
|
||||
|
||||
// QA Team Permissions
|
||||
{ role: 'qa_team', permission: 'test_components', resource: 'components', action: 'read' },
|
||||
{ role: 'qa_team', permission: 'create_issue', resource: 'issues', action: 'create' },
|
||||
{ role: 'qa_team', permission: 'view_metrics', resource: 'metrics', action: 'read' },
|
||||
{ role: 'qa_team', permission: 'run_esre', resource: 'testing', action: 'create' }
|
||||
].map(s => ({
|
||||
...s,
|
||||
id: uuidv4(),
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp
|
||||
}));
|
||||
|
||||
if (seeds.length > 0) {
|
||||
await queryInterface.bulkInsert('TeamPermissions', seeds, { transaction });
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
console.log('✓ RBAC migration completed successfully');
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
console.error('✗ RBAC migration failed:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
// Drop team_permissions table
|
||||
await queryInterface.dropTable('TeamPermissions', { transaction });
|
||||
|
||||
// Remove team_id column
|
||||
await queryInterface.removeColumn('Users', 'team_id', { transaction });
|
||||
|
||||
// Restore old role enum
|
||||
await queryInterface.addColumn('Users', 'role_old', {
|
||||
type: DataTypes.ENUM('admin', 'designer', 'developer', 'viewer'),
|
||||
allowNull: true
|
||||
}, { transaction });
|
||||
|
||||
// Map new roles back to old roles
|
||||
await queryInterface.sequelize.query(`
|
||||
UPDATE "Users" SET role_old = CASE
|
||||
WHEN role = 'admin' THEN 'admin'
|
||||
WHEN role = 'ux_team' THEN 'designer'
|
||||
WHEN role = 'ui_team' THEN 'developer'
|
||||
WHEN role = 'qa_team' THEN 'viewer'
|
||||
ELSE 'designer'
|
||||
END
|
||||
`, { transaction });
|
||||
|
||||
await queryInterface.removeColumn('Users', 'role', { transaction });
|
||||
await queryInterface.renameColumn('Users', 'role_old', 'role', { transaction });
|
||||
|
||||
await queryInterface.changeColumn('Users', 'role', {
|
||||
type: DataTypes.ENUM('admin', 'designer', 'developer', 'viewer'),
|
||||
allowNull: false,
|
||||
defaultValue: 'designer'
|
||||
}, { transaction });
|
||||
|
||||
// Cleanup Enums
|
||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_Users_role_new";', { transaction });
|
||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_TeamPermissions_role";', { transaction });
|
||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_TeamPermissions_action";', { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
console.log('✓ RBAC migration rolled back successfully');
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
console.error('✗ RBAC migration rollback failed:', err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
97
server/migrations/20251209000001-add-config-settings.js
Normal file
97
server/migrations/20251209000001-add-config-settings.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import { DataTypes, Op } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await queryInterface.createTable('config_settings', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
scope: {
|
||||
type: DataTypes.ENUM('SYSTEM', 'PROJECT', 'USER'),
|
||||
allowNull: false
|
||||
},
|
||||
scopeId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: true,
|
||||
field: 'scope_id'
|
||||
},
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
value: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: false
|
||||
},
|
||||
isSecret: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
field: 'is_secret'
|
||||
},
|
||||
schemaVersion: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1,
|
||||
field: 'schema_version'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
},
|
||||
updatedAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'updated_at'
|
||||
}
|
||||
}, { transaction });
|
||||
|
||||
// Create a unique index that handles the NULL scope_id for SYSTEM scope correctly
|
||||
// In Postgres 15+ we could use NULLS NOT DISTINCT, but for compatibility we use a partial index
|
||||
// for the NULL case and a standard unique index for the non-NULL case.
|
||||
|
||||
// 1. Standard unique constraint for non-null scope_id (Project/User)
|
||||
await queryInterface.addIndex('config_settings', ['scope', 'scope_id', 'key'], {
|
||||
unique: true,
|
||||
where: {
|
||||
scope_id: { [Op.ne]: null }
|
||||
},
|
||||
name: 'config_settings_scope_scope_id_key_unique',
|
||||
transaction
|
||||
});
|
||||
|
||||
// 2. Unique constraint for SYSTEM scope (where scope_id is NULL)
|
||||
await queryInterface.addIndex('config_settings', ['scope', 'key'], {
|
||||
unique: true,
|
||||
where: {
|
||||
scope_id: null
|
||||
},
|
||||
name: 'config_settings_system_scope_key_unique',
|
||||
transaction
|
||||
});
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.dropTable('config_settings', { transaction });
|
||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_config_settings_scope";', { transaction });
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
78
server/migrations/20251209000002-add-config-audit-log.js
Normal file
78
server/migrations/20251209000002-add-config-audit-log.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await queryInterface.createTable('config_audit_logs', {
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
configId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: 'config_id',
|
||||
references: {
|
||||
model: 'config_settings',
|
||||
key: 'id'
|
||||
},
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
actorId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
field: 'actor_id',
|
||||
references: {
|
||||
model: 'users', // Note: User table name usually lowercase in Postgres if created via Sequelize defaults, or "Users" if strictly quoted
|
||||
key: 'id'
|
||||
}
|
||||
},
|
||||
action: {
|
||||
type: DataTypes.ENUM('CREATE', 'UPDATE', 'DELETE'),
|
||||
allowNull: false
|
||||
},
|
||||
previousValue: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
field: 'previous_value'
|
||||
},
|
||||
newValue: {
|
||||
type: DataTypes.JSONB,
|
||||
allowNull: true,
|
||||
field: 'new_value'
|
||||
},
|
||||
createdAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
field: 'created_at'
|
||||
}
|
||||
}, { transaction });
|
||||
|
||||
// Add indexes for common query patterns
|
||||
await queryInterface.addIndex('config_audit_logs', ['config_id'], { transaction });
|
||||
await queryInterface.addIndex('config_audit_logs', ['actor_id'], { transaction });
|
||||
await queryInterface.addIndex('config_audit_logs', ['created_at'], { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.dropTable('config_audit_logs', { transaction });
|
||||
await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_config_audit_logs_action";', { transaction });
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export default {
|
||||
up: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
const now = new Date();
|
||||
|
||||
const defaultConfigs = [
|
||||
{
|
||||
key: 'figma.api_timeout',
|
||||
value: JSON.stringify(30000),
|
||||
is_secret: false,
|
||||
scope: 'SYSTEM',
|
||||
scope_id: null
|
||||
},
|
||||
{
|
||||
key: 'storybook.default_port',
|
||||
value: JSON.stringify(6006),
|
||||
is_secret: false,
|
||||
scope: 'SYSTEM',
|
||||
scope_id: null
|
||||
},
|
||||
{
|
||||
key: 'rate_limit.requests_per_minute',
|
||||
value: JSON.stringify(60),
|
||||
is_secret: false,
|
||||
scope: 'SYSTEM',
|
||||
scope_id: null
|
||||
},
|
||||
{
|
||||
key: 'theme',
|
||||
value: JSON.stringify('auto'),
|
||||
is_secret: false,
|
||||
scope: 'SYSTEM',
|
||||
scope_id: null
|
||||
}
|
||||
];
|
||||
|
||||
const records = defaultConfigs.map(config => ({
|
||||
id: uuidv4(),
|
||||
...config,
|
||||
schema_version: 1,
|
||||
created_at: now,
|
||||
updated_at: now
|
||||
}));
|
||||
|
||||
await queryInterface.bulkInsert('config_settings', records, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
console.error('Failed to seed default configs:', err);
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.bulkDelete('config_settings', { scope: 'SYSTEM' }, { transaction });
|
||||
await transaction.commit();
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user