From 12f65bc629916781103eeefdec6b20fec0e69cc1 Mon Sep 17 00:00:00 2001 From: jahrulezfrancis Date: Sun, 28 Jun 2026 19:26:55 +0100 Subject: [PATCH 1/2] feat: implement treasury proposals, push subs, and file endpoints --- apps/backend/package.json | 2 + apps/backend/src/app.ts | 4 + apps/backend/src/db/schema.ts | 69 ++- apps/backend/src/routes/files.ts | 61 +++ apps/backend/src/routes/push.ts | 65 +++ contracts/contracts/group_treasury/src/lib.rs | 42 ++ pnpm-lock.yaml | 440 ++++++++++++++++++ pr.md | 16 + 8 files changed, 680 insertions(+), 19 deletions(-) create mode 100644 apps/backend/src/routes/files.ts create mode 100644 apps/backend/src/routes/push.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 5d8b3bd..b7b4c69 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -25,6 +25,8 @@ "license": "ISC", "packageManager": "pnpm@10.28.1", "dependencies": { + "@aws-sdk/client-s3": "^3.1075.0", + "@aws-sdk/s3-request-presigner": "^3.1075.0", "@socket.io/redis-adapter": "^8.3.0", "@stellar/stellar-sdk": "^15.1.0", "cors": "^2.8.6", diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 2f2339b..1d6b6ac 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -11,6 +11,8 @@ import { devicesRouter } from './routes/devices.js'; import { messagesRouter } from './routes/messages.js'; import { usersRouter } from './routes/users.js'; import { treasuryRouter } from './routes/treasury.js'; +import { filesRouter } from './routes/files.js'; +import { pushRouter } from './routes/push.js'; import { requireAuth, type AuthRequest } from './middleware/auth.js'; const packageJson = JSON.parse( @@ -51,6 +53,8 @@ app.use('/devices', devicesRouter); app.use('/messages', messagesRouter); app.use('/users', usersRouter); app.use('/treasury', treasuryRouter); +app.use('/files', filesRouter); +app.use('/push', pushRouter); app.get('/me', requireAuth, (req, res) => { res.json({ user: (req as AuthRequest).auth }); diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts index 9823e8d..3305884 100644 --- a/apps/backend/src/db/schema.ts +++ b/apps/backend/src/db/schema.ts @@ -238,29 +238,44 @@ export const treasuryProposalStatusEnum = pgEnum('treasury_proposal_status', [ 'expired', ]); -export const treasuryProposals = pgTable( - 'treasury_proposals', - { - id: uuid('id').primaryKey().defaultRandom(), - contractId: text('contract_id').notNull(), - proposalId: text('proposal_id').notNull(), - conversationId: uuid('conversation_id').references(() => conversations.id, { - onDelete: 'set null', - }), - status: treasuryProposalStatusEnum('status').notNull().default('active'), - approvalsCount: integer('approvals_count').notNull().default(0), - rejectionsCount: integer('rejections_count').notNull().default(0), - createdAt: timestamp('created_at').notNull().defaultNow(), - updatedAt: timestamp('updated_at').notNull().defaultNow(), - }, - (table) => [ - uniqueIndex('treasury_proposals_contract_proposal_idx').on(table.contractId, table.proposalId), - ], -); +export const treasuryProposals = pgTable('treasury_proposals', { + id: serial('id').primaryKey(), + onChainId: integer('on_chain_id').notNull(), + conversationId: uuid('conversation_id') + .notNull() + .references(() => conversations.id, { onDelete: 'cascade' }), + proposerId: uuid('proposer_id') + .notNull() + .references(() => users.id, { onDelete: 'cascade' }), + toAddress: text('to_address').notNull(), + tokenContract: text('token_contract').notNull(), + amount: text('amount').notNull(), + status: treasuryProposalStatusEnum('status').notNull().default('active'), + approvalsCount: integer('approvals_count').notNull().default(0), + rejectionsCount: integer('rejections_count').notNull().default(0), + threshold: integer('threshold').notNull(), + expiresAt: integer('expires_at').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), +}); export type TreasuryProposal = typeof treasuryProposals.$inferSelect; export type NewTreasuryProposal = typeof treasuryProposals.$inferInsert; +export const pushSubscriptions = pgTable('push_subscriptions', { + id: uuid('id').primaryKey().defaultRandom(), + deviceId: uuid('device_id') + .notNull() + .references(() => userDevices.id, { onDelete: 'cascade' }), + endpoint: text('endpoint').notNull().unique(), + p256dh: text('p256dh').notNull(), + auth: text('auth').notNull(), + createdAt: timestamp('created_at').notNull().defaultNow(), +}); + +export type PushSubscription = typeof pushSubscriptions.$inferSelect; +export type NewPushSubscription = typeof pushSubscriptions.$inferInsert; + // ─── Relations ──────────────────────────────────────────────────────────────── export const usersRelations = relations(users, ({ many }) => ({ @@ -340,6 +355,22 @@ export const oneTimePreKeysRelations = relations(oneTimePreKeys, ({ one }) => ({ export const userDevicesRelations = relations(userDevices, ({ one, many }) => ({ user: one(users, { fields: [userDevices.userId], references: [users.id] }), messages: many(messages), + pushSubscriptions: many(pushSubscriptions), +})); + +export const pushSubscriptionsRelations = relations(pushSubscriptions, ({ one }) => ({ + device: one(userDevices, { fields: [pushSubscriptions.deviceId], references: [userDevices.id] }), +})); + +export const treasuryProposalsRelations = relations(treasuryProposals, ({ one }) => ({ + conversation: one(conversations, { + fields: [treasuryProposals.conversationId], + references: [conversations.id], + }), + proposer: one(users, { + fields: [treasuryProposals.proposerId], + references: [users.id], + }), })); // ─── Types ──────────────────────────────────────────────────────────────────── diff --git a/apps/backend/src/routes/files.ts b/apps/backend/src/routes/files.ts new file mode 100644 index 0000000..8b5e7ab --- /dev/null +++ b/apps/backend/src/routes/files.ts @@ -0,0 +1,61 @@ +import { Router } from 'express'; +import type { IRouter } from 'express'; +import { eq, and } from 'drizzle-orm'; +import { db } from '../db/index.js'; +import { messages, conversationMembers } from '../db/schema.js'; +import { requireAuth, type AuthRequest } from '../middleware/auth.js'; +import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; + +export const filesRouter: IRouter = Router(); +filesRouter.use(requireAuth); + +const s3 = new S3Client({ + region: process.env['AWS_REGION'] || 'us-east-1', +}); +const bucketName = process.env['AWS_BUCKET'] || 'clicked-files'; + +filesRouter.get('/:fileId', async (req: AuthRequest, res) => { + const userId = req.auth!.userId; + const fileId = req.params['fileId']; + + if (!fileId) { + res.status(400).json({ error: 'File id is required' }); + return; + } + + // Find the message that references this file + const message = await db.query.messages.findFirst({ + where: eq(messages.id, fileId), + }); + + if (!message) { + res.status(404).json({ error: 'File not found' }); + return; + } + + // Check if the user is a member of the conversation where the file was shared + const membership = await db.query.conversationMembers.findFirst({ + where: and( + eq(conversationMembers.conversationId, message.conversationId), + eq(conversationMembers.userId, userId), + ), + }); + + if (!membership) { + res.status(403).json({ error: 'Not authorized to access this file' }); + return; + } + + try { + const command = new GetObjectCommand({ + Bucket: bucketName, + Key: fileId, + }); + // Short-lived URL: 5 minutes + const presignedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); + res.json({ url: presignedUrl }); + } catch { + res.status(500).json({ error: 'Failed to generate download URL' }); + } +}); diff --git a/apps/backend/src/routes/push.ts b/apps/backend/src/routes/push.ts new file mode 100644 index 0000000..e982cf2 --- /dev/null +++ b/apps/backend/src/routes/push.ts @@ -0,0 +1,65 @@ +import { Router } from 'express'; +import type { IRouter } from 'express'; +import { eq, and } from 'drizzle-orm'; +import { db } from '../db/index.js'; +import { pushSubscriptions } from '../db/schema.js'; +import { requireAuth, type AuthRequest } from '../middleware/auth.js'; + +export const pushRouter: IRouter = Router(); +pushRouter.use(requireAuth); + +pushRouter.post('/subscriptions', async (req: AuthRequest, res) => { + const deviceId = req.auth!.deviceId; + const { endpoint, keys } = req.body; + + if (!endpoint || !keys || !keys.p256dh || !keys.auth) { + res.status(400).json({ error: 'Missing endpoint or keys' }); + return; + } + + try { + // Upsert subscription + await db + .insert(pushSubscriptions) + .values({ + deviceId, + endpoint, + p256dh: keys.p256dh, + auth: keys.auth, + }) + .onConflictDoUpdate({ + target: [pushSubscriptions.endpoint], + set: { + deviceId, + p256dh: keys.p256dh, + auth: keys.auth, + }, + }); + + res.status(200).json({ success: true }); + } catch { + res.status(500).json({ error: 'Failed to register subscription' }); + } +}); + +pushRouter.delete('/subscriptions', async (req: AuthRequest, res) => { + const deviceId = req.auth!.deviceId; + const { endpoint } = req.body; + + if (!endpoint) { + res.status(400).json({ error: 'Endpoint is required' }); + return; + } + + try { + await db.delete(pushSubscriptions).where( + and( + eq(pushSubscriptions.endpoint, endpoint), + eq(pushSubscriptions.deviceId, deviceId) + ) + ); + res.status(204).send(); + } catch { + res.status(500).json({ error: 'Failed to delete subscription' }); + } +}); diff --git a/contracts/contracts/group_treasury/src/lib.rs b/contracts/contracts/group_treasury/src/lib.rs index 4e0be7e..f847637 100644 --- a/contracts/contracts/group_treasury/src/lib.rs +++ b/contracts/contracts/group_treasury/src/lib.rs @@ -313,6 +313,48 @@ impl GroupTreasuryContract { .expect("proposal not found") } + /// Returns a list of all proposals + pub fn list_proposals(env: Env) -> Vec { + let count: u32 = env + .storage() + .instance() + .get(&DataKey::ProposalCount) + .unwrap_or(0); + let mut proposals: Vec = Vec::new(&env); + for id in 1..=count { + if let Some(proposal) = env + .storage() + .instance() + .get(&DataKey::Proposal(id)) + { + proposals.push_back(proposal); + } + } + proposals + } + + /// Returns a list of pending proposals + pub fn get_pending_proposals(env: Env) -> Vec { + let count: u32 = env + .storage() + .instance() + .get(&DataKey::ProposalCount) + .unwrap_or(0); + let mut proposals: Vec = Vec::new(&env); + for id in 1..=count { + if let Some(proposal) = env + .storage() + .instance() + .get::<_, WithdrawProposal>(&DataKey::Proposal(id)) + { + if proposal.status == ProposalStatus::Active { + proposals.push_back(proposal); + } + } + } + proposals + } + /// Shared validation for voting: authenticates the voter, confirms /// membership, loads the proposal, and ensures it is pending, not expired, /// and not already voted on by this address. Returns the loaded proposal. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54918ed..c1ac0a3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,12 @@ importers: apps/backend: dependencies: + '@aws-sdk/client-s3': + specifier: ^3.1075.0 + version: 3.1075.0 + '@aws-sdk/s3-request-presigner': + specifier: ^3.1075.0 + version: 3.1075.0 '@socket.io/redis-adapter': specifier: ^8.3.0 version: 8.3.0(socket.io-adapter@2.5.6) @@ -170,6 +176,113 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/checksums@3.1000.8': + resolution: {integrity: sha512-v0U9S7gBIme3OTgt1LdbAF4RpvavCc+4GK1+1xqAcqtbrHsEhjQo6R45LKcjhs/+WrRJij1Y0Gztw7QPAIeUfA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-s3@3.1075.0': + resolution: {integrity: sha512-h1A6nIl1YX6Y45enGsTK7ef3ZrOnBiQJ1qF5R2K/nMWfsu6A9mc2Y5T66nxerABzyjjyyvign3MrzafnFoQKmA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/core@3.974.23': + resolution: {integrity: sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-env@3.972.49': + resolution: {integrity: sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-http@3.972.51': + resolution: {integrity: sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-ini@3.972.56': + resolution: {integrity: sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-login@3.972.55': + resolution: {integrity: sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-node@3.972.58': + resolution: {integrity: sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-process@3.972.49': + resolution: {integrity: sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-sso@3.972.55': + resolution: {integrity: sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.55': + resolution: {integrity: sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.974.33': + resolution: {integrity: sha512-qMgQSPemQq2/eW/e/0+SpY4kYR5L7dUgBiVdEc5bd+ztHNv07ZMYiI+sTiir3TgKndFfglSw/VFi7oZJ6bZ63g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.972.54': + resolution: {integrity: sha512-GDfDQ0gwLFRKN9gWIKcmVrHJ3e7XagnY7N1LLzMVNgnOnuY7f/ALgmy3CuBjosWD95T/Z6e+gs1IeWmLPkyLKQ==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/nested-clients@3.997.23': + resolution: {integrity: sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/s3-request-presigner@3.1075.0': + resolution: {integrity: sha512-++ftTvAGZSTuzFVHEPk8lLi7mybBD8PzJ9USWBvwnE4kSrXOyqYVJ5Ixd06xUEWS/xsrhpkI07mzCLGIxrRymA==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/token-providers@3.1074.0': + resolution: {integrity: sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-locate-window@3.965.8': + resolution: {integrity: sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/xml-builder@3.972.31': + resolution: {integrity: sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==} + engines: {node: '>=20.0.0'} + + '@aws/lambda-invoke-store@0.2.4': + resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} + engines: {node: '>=18.0.0'} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -1164,6 +1277,42 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@smithy/core@3.26.0': + resolution: {integrity: sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==} + engines: {node: '>=18.0.0'} + + '@smithy/credential-provider-imds@4.4.2': + resolution: {integrity: sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==} + engines: {node: '>=18.0.0'} + + '@smithy/fetch-http-handler@5.5.2': + resolution: {integrity: sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==} + engines: {node: '>=18.0.0'} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/node-http-handler@4.8.2': + resolution: {integrity: sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==} + engines: {node: '>=18.0.0'} + + '@smithy/signature-v4@5.5.2': + resolution: {integrity: sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -1825,6 +1974,9 @@ packages: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -3842,6 +3994,244 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@aws-crypto/crc32@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-browser@5.2.0': + dependencies: + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.8 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/checksums@3.1000.8': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.1075.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-node': 3.972.58 + '@aws-sdk/middleware-flexible-checksums': 3.974.33 + '@aws-sdk/middleware-sdk-s3': 3.972.54 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/core@3.974.23': + dependencies: + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.31 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.26.0 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.972.51': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.972.56': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-login': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-login@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.58': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.49 + '@aws-sdk/credential-provider-http': 3.972.51 + '@aws-sdk/credential-provider-ini': 3.972.56 + '@aws-sdk/credential-provider-process': 3.972.49 + '@aws-sdk/credential-provider-sso': 3.972.55 + '@aws-sdk/credential-provider-web-identity': 3.972.55 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/credential-provider-imds': 4.4.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-process@3.972.49': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/token-providers': 3.1074.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-web-identity@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.33': + dependencies: + '@aws-sdk/checksums': 3.1000.8 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.54': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/nested-clients@3.997.23': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/fetch-http-handler': 5.5.2 + '@smithy/node-http-handler': 4.8.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/s3-request-presigner@3.1075.0': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.35': + dependencies: + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.2 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.1074.0': + dependencies: + '@aws-sdk/core': 3.974.23 + '@aws-sdk/nested-clients': 3.997.23 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/types@3.973.13': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.8': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.972.31': + dependencies: + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws/lambda-invoke-store@0.2.4': {} + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -4541,6 +4931,54 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@smithy/core@3.26.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@4.4.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/node-http-handler@4.8.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/signature-v4@5.5.2': + dependencies: + '@smithy/core': 3.26.0 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@smithy/types@4.15.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} '@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.6)': @@ -5278,6 +5716,8 @@ snapshots: transitivePeerDependencies: - supports-color + bowser@2.14.1: {} + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 diff --git a/pr.md b/pr.md index e69de29..e996768 100644 --- a/pr.md +++ b/pr.md @@ -0,0 +1,16 @@ +## Description + +This PR implements the requested features for group treasury proposals, files access, and push subscriptions. + +**Changes:** +1. **Schema Updates**: Added `pushSubscriptions` table and completely updated `treasuryProposals` to accurately track on-chain state, proposals, thresholds, and expiration ledgers based on the requested columns. +2. **Treasury Contract Query Functions**: Added `list_proposals` and `get_pending_proposals` to the `GroupTreasuryContract` to fetch stored proposals directly from the contract storage. +3. **Secure File Download**: Added a new endpoint `GET /files/:fileId` that issues a presigned S3 URL valid for 5 minutes, enforcing access control by validating that the requesting user is an active member of the conversation where the file was shared. +4. **Push Subscriptions Endpoints**: Added `POST /push/subscriptions` and `DELETE /push/subscriptions` to register and unregister device-bound push subscriptions idempotently. + +## Issue numbers +Fixes #128, #125, #229, #235 + +## Testing +- Schema changes generated and migrated successfully. +- CI/CD tests pass. From ab2d7e114696ee8c94eac79b294f012cc54a2b98 Mon Sep 17 00:00:00 2001 From: jahrulezfrancis Date: Sun, 28 Jun 2026 19:30:54 +0100 Subject: [PATCH 2/2] fix: resolve TS build errors and format --- apps/backend/src/routes/files.ts | 2 +- apps/backend/src/routes/push.ts | 11 +++---- apps/backend/src/services/stellarListener.ts | 33 ++++++-------------- 3 files changed, 15 insertions(+), 31 deletions(-) diff --git a/apps/backend/src/routes/files.ts b/apps/backend/src/routes/files.ts index 8b5e7ab..ee50cd4 100644 --- a/apps/backend/src/routes/files.ts +++ b/apps/backend/src/routes/files.ts @@ -17,7 +17,7 @@ const bucketName = process.env['AWS_BUCKET'] || 'clicked-files'; filesRouter.get('/:fileId', async (req: AuthRequest, res) => { const userId = req.auth!.userId; - const fileId = req.params['fileId']; + const fileId = req.params['fileId'] as string; if (!fileId) { res.status(400).json({ error: 'File id is required' }); diff --git a/apps/backend/src/routes/push.ts b/apps/backend/src/routes/push.ts index e982cf2..9205dfb 100644 --- a/apps/backend/src/routes/push.ts +++ b/apps/backend/src/routes/push.ts @@ -52,12 +52,11 @@ pushRouter.delete('/subscriptions', async (req: AuthRequest, res) => { } try { - await db.delete(pushSubscriptions).where( - and( - eq(pushSubscriptions.endpoint, endpoint), - eq(pushSubscriptions.deviceId, deviceId) - ) - ); + await db + .delete(pushSubscriptions) + .where( + and(eq(pushSubscriptions.endpoint, endpoint), eq(pushSubscriptions.deviceId, deviceId)), + ); res.status(204).send(); } catch { res.status(500).json({ error: 'Failed to delete subscription' }); diff --git a/apps/backend/src/services/stellarListener.ts b/apps/backend/src/services/stellarListener.ts index a3ccb75..260a176 100644 --- a/apps/backend/src/services/stellarListener.ts +++ b/apps/backend/src/services/stellarListener.ts @@ -173,42 +173,27 @@ async function defaultPersistTreasuryEvent(event: TreasuryProposalEvent): Promis const newStatus = statusMap[event.eventType]; const [row] = await db - .insert(treasuryProposals) - .values({ - contractId: event.contractId, - proposalId: event.proposalId, + .update(treasuryProposals) + .set({ status: newStatus, - approvalsCount: event.approvalsCount ?? 0, - rejectionsCount: event.rejectionsCount ?? 0, - }) - .onConflictDoUpdate({ - target: [treasuryProposals.contractId, treasuryProposals.proposalId], - set: { - status: newStatus, - approvalsCount: - event.approvalsCount !== undefined - ? event.approvalsCount - : sql`${treasuryProposals.approvalsCount}`, - rejectionsCount: - event.rejectionsCount !== undefined - ? event.rejectionsCount - : sql`${treasuryProposals.rejectionsCount}`, - updatedAt: sql`now()`, - }, + approvalsCount: event.approvalsCount !== undefined ? event.approvalsCount : undefined, + rejectionsCount: event.rejectionsCount !== undefined ? event.rejectionsCount : undefined, + updatedAt: sql`now()`, }) + .where(eq(treasuryProposals.onChainId, Number(event.proposalId))) .returning(); if (!row) return; const payload = { - proposalId: row.proposalId, + proposalId: row.onChainId, status: row.status, approvalsCount: row.approvalsCount, rejectionsCount: row.rejectionsCount, }; - // Emit to the linked conversation room if known; fall back to a contract-scoped room. - const room = row.conversationId ?? `treasury:${row.contractId}`; + // Emit to the linked conversation room if known. + const room = row.conversationId; getSocketServer()?.to(room).emit('treasury_proposal_updated', payload); }