Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
73 changes: 73 additions & 0 deletions prisma/migrations/20250808020526_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
Warnings:

- The primary key for the `UserVolunteer` table will be changed. If it partially fails, the table could be left without primary key constraint.
- You are about to drop the column `id` on the `UserVolunteer` table. All the data in the column will be lost.

*/
-- DropForeignKey
ALTER TABLE "Certificate" DROP CONSTRAINT "Certificate_volunteerId_fkey";

-- DropForeignKey
ALTER TABLE "CertificateDownloadLog" DROP CONSTRAINT "CertificateDownloadLog_certificateId_fkey";

-- DropForeignKey
ALTER TABLE "NFT" DROP CONSTRAINT "NFT_organizationId_fkey";

-- DropForeignKey
ALTER TABLE "NFT" DROP CONSTRAINT "NFT_userId_fkey";

-- DropForeignKey
ALTER TABLE "Project" DROP CONSTRAINT "Project_organizationId_fkey";

-- DropForeignKey
ALTER TABLE "UserVolunteer" DROP CONSTRAINT "UserVolunteer_userId_fkey";

-- DropForeignKey
ALTER TABLE "UserVolunteer" DROP CONSTRAINT "UserVolunteer_volunteerId_fkey";

-- DropForeignKey
ALTER TABLE "Volunteer" DROP CONSTRAINT "Volunteer_projectId_fkey";

-- DropForeignKey
ALTER TABLE "escrows" DROP CONSTRAINT "escrows_user_id_fkey";

-- DropIndex
DROP INDEX "UserVolunteer_userId_volunteerId_key";

Comment on lines +8 to +37
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Referential integrity lost – foreign keys dropped but never re-added

The migration removes every FK on Certificate, NFT, Project, UserVolunteer, Volunteer and escrows yet does not recreate them afterwards.
This leaves the DB without critical constraints, allowing orphaned rows and breaking Prisma’s relation expectations.

Re-add the FKs (with ON DELETE … clauses matching domain rules) in the same migration or a follow-up script before shipping.

🤖 Prompt for AI Agents
In prisma/migrations/20250808020526_init/migration.sql between lines 8 and 37,
the migration drops multiple foreign key constraints but does not re-add them,
which breaks referential integrity. To fix this, add SQL statements after the
DROP CONSTRAINT commands to recreate each foreign key constraint with
appropriate ON DELETE rules that reflect the domain logic, ensuring the database
maintains proper relational integrity and aligns with Prisma's expectations.

-- AlterTable
ALTER TABLE "Project" ADD COLUMN "status" TEXT NOT NULL DEFAULT 'active';

-- AlterTable
ALTER TABLE "User" ADD COLUMN "verificationTokenExpires" TIMESTAMP(3);

-- AlterTable
ALTER TABLE "UserVolunteer" DROP CONSTRAINT "UserVolunteer_pkey",
DROP COLUMN "id",
ADD COLUMN "hoursContributed" DOUBLE PRECISION NOT NULL DEFAULT 0,
ADD CONSTRAINT "UserVolunteer_pkey" PRIMARY KEY ("userId", "volunteerId");

-- AlterTable
ALTER TABLE "Volunteer" ADD COLUMN "maxVolunteers" INTEGER NOT NULL DEFAULT 10;

-- CreateTable
CREATE TABLE "Message" (
"id" TEXT NOT NULL,
"content" TEXT NOT NULL,
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"readAt" TIMESTAMP(3),
"senderId" TEXT NOT NULL,
"receiverId" TEXT NOT NULL,
"volunteerId" TEXT NOT NULL,

CONSTRAINT "Message_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "Message_senderId_idx" ON "Message"("senderId");

-- CreateIndex
CREATE INDEX "Message_receiverId_idx" ON "Message"("receiverId");

-- CreateIndex
CREATE INDEX "Message_volunteerId_idx" ON "Message"("volunteerId");
52 changes: 0 additions & 52 deletions src/modules/auth/presentation/controllers/Auth.controller.stub.ts

This file was deleted.

172 changes: 172 additions & 0 deletions src/modules/auth/presentation/controllers/Auth.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { Request, Response } from "express";

// imports for DTO validator
import { plainToInstance } from "class-transformer";
import { validate } from "class-validator";

// Necessary DTOs
import { RegisterDto } from "../../dto/register.dto";
import { LoginDto } from "../../dto/login.dto";
import { ResendVerificationDTO } from "../../dto/resendVerificationDTO";
import {
VerifyWalletDto,
ValidateWalletFormatDto,
} from "../../dto/wallet-validation.dto";

// Use cases
import { PrismaUserRepository } from "../../../user/repositories/PrismaUserRepository";
import { SendVerificationEmailUseCase } from "../../use-cases/send-verification-email.usecase";
import { ResendVerificationEmailUseCase } from "../../use-cases/resend-verification-email.usecase";
import { VerifyEmailUseCase } from "../../use-cases/verify-email.usecase";
import { ValidateWalletFormatUseCase } from "../../use-cases/wallet-format-validation.usecase";
import { VerifyWalletUseCase } from "../../use-cases/verify-wallet.usecase";

const userRepository = new PrismaUserRepository();
const sendVerificationEmailUseCase = new SendVerificationEmailUseCase(
userRepository
);
const resendVerificationEmailUseCase = new ResendVerificationEmailUseCase(
userRepository
);
const verifyEmailUseCase = new VerifyEmailUseCase(userRepository);
const validateWalletFormatUseCase = new ValidateWalletFormatUseCase();
const verifyWalletUseCase = new VerifyWalletUseCase();

// DTO validator
async function validateOr400<T>(
Cls: new () => T,
payload: unknown,
res: Response
): Promise<T | undefined> {
const dto = plainToInstance(Cls, payload);
const errors = await validate(dto as object, {
whitelist: true,
forbidNonWhitelisted: true,
});

// dto not verified, throw a Bad Request
if (errors.length) {
res.status(400).json({ message: "Validation failed", errors });
return;
}

return dto;
}

const register = async (req: Request, res: Response) => {
const dto = await validateOr400(RegisterDto, req.body, res);
if (!dto) return;

try {
// Send verification email to provided address
await sendVerificationEmailUseCase.execute({ email: dto.email });
res.status(200).json({ message: "Verification email sent" });
} catch (err) {
const message =
err instanceof Error ? err.message : "Failed to send verification email";
const status = message === "User not found" ? 400 : 500;
res.status(status).json({ error: message });
}
};

const login = async (req: Request, res: Response) => {
const dto = await validateOr400(LoginDto, req.body, res);
if (!dto) return;

// TODO: Implement Wallet auth logic as a use case
res.status(501).json({
message: "Login service temporarily disabled",
error: "Wallet auth logic not implemented yet",
});
};

const resendVerificationEmail = async (req: Request, res: Response) => {
const dto = await validateOr400(ResendVerificationDTO, req.body, res);
if (!dto) return;

try {
// Resends verification email to provided address
await resendVerificationEmailUseCase.execute({ email: dto.email });
res.status(200).json({ message: "Verification email resent" });
} catch (err) {
const message =
err instanceof Error
? err.message
: "Failed to resend verification email";
const status = message === "User not found" ? 404 : 500;
res.status(status).json({ error: message });
}
};

const verifyEmail = async (req: Request, res: Response) => {
const tokenParam =
typeof req.params.token === "string" ? req.params.token : undefined;
const tokenQuery =
typeof req.query.token === "string"
? (req.query.token as string)
: undefined;
const token = tokenParam || tokenQuery;

// if token is not given in the request
if (!token) {
res.status(400).json({
success: false,
message: "Token in URL is required",
verified: false,
});
return;
}

try {
// Verifies email using use case
const result = await verifyEmailUseCase.execute({ token });
const status = result.success ? 200 : 400;
res.status(status).json(result);
} catch {
res.status(400).json({
success: false,
message: "Invalid or expired verification token",
verified: false,
});
}
};

const verifyWallet = async (req: Request, res: Response) => {
const dto = await validateOr400(VerifyWalletDto, req.body, res);
if (!dto) return;

try {
const result = await verifyWalletUseCase.execute(dto);
const status = result.verified ? 200 : 400;
res.status(status).json(result);
} catch (err) {
const message =
err instanceof Error ? err.message : "Wallet verification failed";
res.status(500).json({ error: message });
}
};

const validateWalletFormat = async (req: Request, res: Response) => {
const dto = await validateOr400(ValidateWalletFormatDto, req.body, res);
if (!dto) return;

try {
// Validates wallet format using use case
const result = await validateWalletFormatUseCase.execute(dto);
const status = result.valid ? 200 : 400;
res.status(status).json(result);
} catch (err) {
const message =
err instanceof Error ? err.message : "Wallet format validation failed";
res.status(500).json({ error: message });
}
};

export default {
register,
login,
resendVerificationEmail,
verifyEmail,
verifyWallet,
validateWalletFormat,
};
84 changes: 84 additions & 0 deletions src/modules/auth/use-cases/verify-wallet.usecase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Keypair, StrKey, Horizon } from "@stellar/stellar-sdk";
import { VerifyWalletDto } from "../dto/wallet-validation.dto";
import { horizonConfig } from "../../../config/horizon.config";

type WalletVerificationResult = {
verified: boolean;
walletAddress: string;
error?: string;
};

export class VerifyWalletUseCase {
async execute(input: VerifyWalletDto): Promise<WalletVerificationResult> {
const { walletAddress, signature, message } = input;

// Validate public key format
if (!StrKey.isValidEd25519PublicKey(walletAddress)) {
return {
verified: false,
walletAddress,
error: "Invalid Stellar public key",
};
}

// Check that account exists on Horizon network before signature verification
try {
const server = new Horizon.Server(horizonConfig.url, {
allowHttp: horizonConfig.url.startsWith("http://"),
});
await server.accounts().accountId(walletAddress).call();
} catch (err: unknown) {
Comment on lines +1 to +30
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Horizon.Server is not part of @stellar/stellar-sdk runtime

@stellar/stellar-sdk exports Server directly; the Horizon namespace only contains types. At runtime new Horizon.Server() will throw TypeError: Cannot read properties of undefined.

-import { Keypair, StrKey, Horizon } from "@stellar/stellar-sdk";
+import { Keypair, StrKey, Server } from "@stellar/stellar-sdk";

-const server = new Horizon.Server(horizonConfig.url, {
+const server = new Server(horizonConfig.url, {
   allowHttp: horizonConfig.url.startsWith("http://"),
 });

Update the import and instantiation to prevent a production crash.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { Keypair, StrKey, Horizon } from "@stellar/stellar-sdk";
import { VerifyWalletDto } from "../dto/wallet-validation.dto";
import { horizonConfig } from "../../../config/horizon.config";
type WalletVerificationResult = {
verified: boolean;
walletAddress: string;
error?: string;
};
export class VerifyWalletUseCase {
async execute(input: VerifyWalletDto): Promise<WalletVerificationResult> {
const { walletAddress, signature, message } = input;
// Validate public key format
if (!StrKey.isValidEd25519PublicKey(walletAddress)) {
return {
verified: false,
walletAddress,
error: "Invalid Stellar public key",
};
}
// Check that account exists on Horizon network before signature verification
try {
const server = new Horizon.Server(horizonConfig.url, {
allowHttp: horizonConfig.url.startsWith("http://"),
});
await server.accounts().accountId(walletAddress).call();
} catch (err: unknown) {
import { Keypair, StrKey, Server } from "@stellar/stellar-sdk";
import { VerifyWalletDto } from "../dto/wallet-validation.dto";
import { horizonConfig } from "../../../config/horizon.config";
type WalletVerificationResult = {
verified: boolean;
walletAddress: string;
error?: string;
};
export class VerifyWalletUseCase {
async execute(input: VerifyWalletDto): Promise<WalletVerificationResult> {
const { walletAddress, signature, message } = input;
// Validate public key format
if (!StrKey.isValidEd25519PublicKey(walletAddress)) {
return {
verified: false,
walletAddress,
error: "Invalid Stellar public key",
};
}
// Check that account exists on Horizon network before signature verification
try {
const server = new Server(horizonConfig.url, {
allowHttp: horizonConfig.url.startsWith("http://"),
});
await server.accounts().accountId(walletAddress).call();
} catch (err: unknown) {
// …existing error handling…
}
}
}
🤖 Prompt for AI Agents
In src/modules/auth/use-cases/verify-wallet.usecase.ts lines 1 to 30, the code
imports Horizon from "@stellar/stellar-sdk" and tries to instantiate the server
with new Horizon.Server(), but Horizon.Server is undefined at runtime causing a
crash. Fix this by importing Server directly from "@stellar/stellar-sdk" and
instantiate it with new Server(...) instead of new Horizon.Server(...).

type HttpError = { response?: { status?: number } };
const httpErr = err as HttpError;

// If account not found on network, error
if (httpErr.response?.status === 404) {
return {
verified: false,
walletAddress,
error: "Account not found on Stellar network",
};
}
return {
verified: false,
walletAddress,
error: err instanceof Error ? err.message : "Horizon query failed",
};
}

// Decode signature (expect base64)
let sig: Buffer;
try {
sig = Buffer.from(signature, "base64");
} catch {
return {
verified: false,
walletAddress,
error: "Invalid signature encoding (base64)",
};
}
if (!sig || sig.length === 0) {
return { verified: false, walletAddress, error: "Empty signature" };
}

const data = Buffer.from(message, "utf8");
const keypair = Keypair.fromPublicKey(walletAddress);

try {
const keypairVerification = keypair.verify(data, sig);
return keypairVerification
? { verified: true, walletAddress }
: {
verified: false,
walletAddress,
error: "Signature verification failed",
};
} catch (err) {
return {
verified: false,
walletAddress,
error: err instanceof Error ? err.message : "Verification error",
};
}
}
}
Loading