Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions backend/migrations/002_create_organizations_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- Create organizations table
CREATE TABLE IF NOT EXISTS organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
logo_url VARCHAR(500),
website_url VARCHAR(500),
discord_url VARCHAR(500),
admin_address VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create index on admin_address for faster lookups
CREATE INDEX idx_organizations_admin_address ON organizations(admin_address);

-- Create index on created_at for sorting
CREATE INDEX idx_organizations_created_at ON organizations(created_at);

-- Add comment to the table
COMMENT ON TABLE organizations IS 'Stores off-chain metadata about organizations that create vaults';
46 changes: 46 additions & 0 deletions backend/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,52 @@ app.get('/api/admin/pending-transfers', async (req, res) => {
}
});

// Organization Routes
app.get('/api/org/:address', async (req, res) => {
try {
const { address } = req.params;

// Validate address format (basic validation)
if (!address || address.length < 20) {
return res.status(400).json({
success: false,
error: 'Valid admin address required'
});
}

const organization = await models.Organization.findOne({
where: { admin_address: address }
});

if (!organization) {
return res.status(404).json({
success: false,
error: 'Organization not found for this admin address'
});
}

res.json({
success: true,
data: {
id: organization.id,
name: organization.name,
logo_url: organization.logo_url,
website_url: organization.website_url,
discord_url: organization.discord_url,
admin_address: organization.admin_address,
created_at: organization.created_at,
updated_at: organization.updated_at
}
});
} catch (error) {
console.error('Error fetching organization:', error);
res.status(500).json({
success: false,
error: error.message
});
}
});

// Start server
const startServer = async () => {
try {
Expand Down
4 changes: 3 additions & 1 deletion backend/src/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ const { sequelize } = require('../database/connection');
const ClaimsHistory = require('./claimsHistory');
const Vault = require('./vault');
const SubSchedule = require('./subSchedule');
const Organization = require('./organization');


const models = {
ClaimsHistory,
Vault,
SubSchedule,
Organization,

sequelize,
};

// Setup associations if needed in the future
// Setup associations
Object.keys(models).forEach((modelName) => {
if (models[modelName].associate) {
models[modelName].associate(models);
Expand Down
60 changes: 60 additions & 0 deletions backend/src/models/organization.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const { DataTypes } = require('sequelize');
const { sequelize } = require('../database/connection');

const Organization = sequelize.define('Organization', {
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
name: {
type: DataTypes.STRING,
allowNull: false,
},
logo_url: {
type: DataTypes.STRING,
allowNull: true,
},
website_url: {
type: DataTypes.STRING,
allowNull: true,
},
discord_url: {
type: DataTypes.STRING,
allowNull: true,
},
admin_address: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
updated_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW,
},
}, {
tableName: 'organizations',
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{
fields: ['admin_address'],
unique: true,
},
],
});

// Add association method
Organization.associate = function(models) {
Organization.hasMany(models.Vault, {
foreignKey: 'org_id',
as: 'vaults'
});
};

module.exports = Organization;
39 changes: 35 additions & 4 deletions backend/src/models/vault.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,35 @@ const Vault = sequelize.define('Vault', {
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},

address: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
},
name: {
type: DataTypes.STRING,
allowNull: true,
},
token_address: {
type: DataTypes.STRING,
allowNull: false,
},
owner_address: {
type: DataTypes.STRING,
allowNull: false,
},
total_amount: {
type: DataTypes.DECIMAL(36, 18),
allowNull: false,
defaultValue: 0,

},
org_id: {
type: DataTypes.UUID,
allowNull: true,
references: {
model: 'organizations',
key: 'id'
}
},
created_at: {
type: DataTypes.DATE,
Expand All @@ -30,15 +52,24 @@ const Vault = sequelize.define('Vault', {
updatedAt: 'updated_at',
indexes: [
{
n
fields: ['address'],
unique: true,
},
{
fields: ['owner_address'],
},
{

fields: ['org_id'],
}
],
});

// Add association method
Vault.associate = function(models) {
Vault.belongsTo(models.Organization, {
foreignKey: 'org_id',
as: 'organization'
});
};

module.exports = Vault;
85 changes: 85 additions & 0 deletions backend/src/swagger/swaggerConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -979,4 +979,89 @@
* $ref: '#/components/schemas/ErrorResponse'
*/

/**
* @swagger
* /api/org/{address}:
* get:
* summary: Get organization info by admin address
* description: Retrieves organization metadata (logo, website, discord) by admin address
* tags: [Organization]
* parameters:
* - in: path
* name: address
* required: true
* schema:
* type: string
* description: Admin address to retrieve organization info for
* responses:
* 200:
* description: Organization info retrieved successfully
* content:
* application/json:
* schema:
* type: object
* properties:
* success:
* type: boolean
* example: true
* data:
* type: object
* properties:
* id:
* type: string
* description: Organization ID
* name:
* type: string
* description: Organization name
* logo_url:
* type: string
* description: URL to organization logo
* website_url:
* type: string
* description: URL to organization website
* discord_url:
* type: string
* description: URL to organization Discord
* admin_address:
* type: string
* description: Admin address linked to this organization
* created_at:
* type: string
* format: date-time
* description: Creation timestamp
* updated_at:
* type: string
* format: date-time
* description: Last update timestamp
* example:
* success: true
* data:
* id: "550e8400-e29b-41d4-a716-446655440000"
* name: "Acme Corp"
* logo_url: "https://acme.com/logo.png"
* website_url: "https://acme.com"
* discord_url: "https://discord.gg/acme"
* admin_address: "0x1234567890123456789012345678901234567890"
* created_at: "2024-01-01T00:00:00.000Z"
* updated_at: "2024-01-01T00:00:00.000Z"
* 400:
* description: Bad request
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 404:
* description: Organization not found
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ErrorResponse'
*/

module.exports = {};
80 changes: 80 additions & 0 deletions backend/test/organization.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
const { sequelize, models } = require('../src/models');

async function testOrganizationModel() {
console.log('🧪 Testing Organization Model Implementation\n');

try {
// Sync the database (this will create the tables if they don't exist)
await sequelize.sync({ force: false }); // Use force: false to avoid dropping existing data

console.log('✅ Database synced successfully');

// Test 1: Create an organization
console.log('\n1. Testing organization creation...');

const testOrg = await models.Organization.create({
name: 'Test Organization',
logo_url: 'https://example.com/logo.png',
website_url: 'https://example.com',
discord_url: 'https://discord.gg/example',
admin_address: '0x1234567890123456789012345678901234567890'
});

console.log('✅ Organization created:', testOrg.toJSON());

// Test 2: Find the organization by admin address
console.log('\n2. Testing organization retrieval by admin address...');

const foundOrg = await models.Organization.findOne({
where: { admin_address: '0x1234567890123456789012345678901234567890' }
});

console.log('✅ Organization retrieved:', foundOrg.toJSON());

// Test 3: Create a vault and link it to the organization
console.log('\n3. Testing vault creation with organization link...');

const testVault = await models.Vault.create({
address: '0xabcdef1234567890abcdef1234567890abcdef1234',
name: 'Test Vault',
token_address: '0xdef1234567890abcdef1234567890abcdef12345',
owner_address: '0x1234567890123456789012345678901234567890',
total_amount: '1000',
org_id: foundOrg.id
});

console.log('✅ Vault created with organization link:', testVault.toJSON());

// Test 4: Test the association - get vaults for the organization
console.log('\n4. Testing organization-vault association...');

const orgWithVaults = await models.Organization.findByPk(foundOrg.id, {
include: [{
model: models.Vault,
as: 'vaults'
}]
});

console.log('✅ Organization with associated vaults:', {
org: orgWithVaults.toJSON(),
vaults: orgWithVaults.vaults.map(v => v.toJSON())
});

console.log('\n🎉 All organization model tests passed!');

// Clean up - delete the test data (optional)
// await testVault.destroy();
// await testOrg.destroy();

} catch (error) {
console.error('❌ Test failed:', error.message);
process.exit(1);
}
}

// Run tests if this file is executed directly
if (require.main === module) {
testOrganizationModel();
}

module.exports = { testOrganizationModel };