Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "phone" TEXT;

-- CreateTable
CREATE TABLE "NotificationPreference" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"emailEnabled" BOOLEAN NOT NULL DEFAULT true,
"smsEnabled" BOOLEAN NOT NULL DEFAULT false,
"pushEnabled" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "NotificationPreference_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "Notification" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"transactionId" TEXT,
"type" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"message" TEXT NOT NULL,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Notification_transactionId_fkey" FOREIGN KEY ("transactionId") REFERENCES "Transaction" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);

-- CreateTable
CREATE TABLE "ContractJob" (
"id" TEXT NOT NULL PRIMARY KEY,
"jobId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"priority" TEXT NOT NULL DEFAULT 'NORMAL',
"status" TEXT NOT NULL DEFAULT 'PENDING',
"contractId" TEXT,
"functionName" TEXT,
"parameters" JSONB,
"result" JSONB,
"error" TEXT,
"errorCategory" TEXT,
"errorSeverity" TEXT,
"errorCode" TEXT,
"userMessage" TEXT,
"suggestedAction" TEXT,
"retryable" BOOLEAN NOT NULL DEFAULT false,
"attempts" INTEGER NOT NULL DEFAULT 0,
"maxAttempts" INTEGER NOT NULL DEFAULT 3,
"createdBy" TEXT,
"metadata" JSONB,
"startedAt" DATETIME,
"completedAt" DATETIME,
"failedAt" DATETIME,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

-- CreateTable
CREATE TABLE "UploadRecord" (
"id" TEXT NOT NULL PRIMARY KEY,
"key" TEXT NOT NULL,
"contentType" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'PENDING',
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "NotificationPreference_userId_key" ON "NotificationPreference"("userId");

-- CreateIndex
CREATE INDEX "NotificationPreference_userId_idx" ON "NotificationPreference"("userId");

-- CreateIndex
CREATE INDEX "Notification_userId_idx" ON "Notification"("userId");

-- CreateIndex
CREATE INDEX "Notification_transactionId_idx" ON "Notification"("transactionId");

-- CreateIndex
CREATE UNIQUE INDEX "ContractJob_jobId_key" ON "ContractJob"("jobId");

-- CreateIndex
CREATE INDEX "ContractJob_jobId_idx" ON "ContractJob"("jobId");

-- CreateIndex
CREATE INDEX "ContractJob_status_idx" ON "ContractJob"("status");

-- CreateIndex
CREATE INDEX "ContractJob_createdBy_idx" ON "ContractJob"("createdBy");

-- CreateIndex
CREATE INDEX "ContractJob_type_idx" ON "ContractJob"("type");

-- CreateIndex
CREATE UNIQUE INDEX "UploadRecord_key_key" ON "UploadRecord"("key");

-- CreateIndex
CREATE INDEX "UploadRecord_status_idx" ON "UploadRecord"("status");
2 changes: 1 addition & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ model Transaction {
feeAssetCode String?
feeType String?

// SEP-31 specific fields (nullable for backward compatibility)
// SEP-31 specific fields
senderInfo Json?
receiverInfo Json?
callbackUrl String?
Expand Down
23 changes: 23 additions & 0 deletions backend/src/api/controllers/sep12.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,3 +365,26 @@ export class Sep12Controller {
}

export const sep12Controller = new Sep12Controller();

// Standalone handler for upload-url (avoids binding issues)
export async function uploadUrl(req: Request, res: Response) {
try {
const { contentType } = req.body;
if (!contentType) {
return res.status(400).json({ error: 'contentType is required' });
}

const key = `kyc/${uuidv4()}`;
const expiresIn = 900; // 15 minutes
const url = await s3StorageProvider.generatePresignedPutUrl(key, contentType, expiresIn);

const record = await prisma.uploadRecord.create({
data: { key, contentType },
});

return res.status(200).json({ url, id: record.id });
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Internal Server Error' });
}
}
9 changes: 9 additions & 0 deletions backend/src/api/routes/sep12.route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ router.post('/customer/upload-url', authMiddleware, validateUploadFileSize, sep1
*/
router.post('/customer/upload-confirm', authMiddleware, sep12Controller.confirmUpload.bind(sep12Controller));

/**
* @swagger
* /sep12/customer/upload-url:
* post:
* summary: Generate a pre-signed S3 URL for document upload
* tags: [SEP-12]
*/
router.post('/customer/upload-url', uploadUrl);

/**
* @swagger
* /sep12/webhook:
Expand Down
39 changes: 39 additions & 0 deletions backend/src/services/storage/s3-storage.provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { S3Client, HeadObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
import { StorageProvider } from './storage-provider.interface';

export class S3StorageProvider implements StorageProvider {
private client: S3Client;
private bucket: string;

constructor() {
this.client = new S3Client({
region: process.env.AWS_REGION ?? 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
},
});
this.bucket = process.env.AWS_S3_BUCKET ?? '';
}

async generatePresignedPutUrl(key: string, contentType: string, expiresIn: number): Promise<string> {
const command = new PutObjectCommand({
Bucket: this.bucket,
Key: key,
ContentType: contentType,
});
return getSignedUrl(this.client, command, { expiresIn });
}

async objectExists(key: string): Promise<boolean> {
try {
await this.client.send(new HeadObjectCommand({ Bucket: this.bucket, Key: key }));
return true;
} catch {
return false;
}
}
}

export const s3StorageProvider = new S3StorageProvider();
4 changes: 4 additions & 0 deletions backend/src/services/storage/storage-provider.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface StorageProvider {
generatePresignedPutUrl(key: string, contentType: string, expiresIn: number): Promise<string>;
objectExists(key: string): Promise<boolean>;
}
Loading