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
112 changes: 69 additions & 43 deletions apps/backend/src/migrations/1780262000000-ImplementFourFeatures.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
import { MigrationInterface, QueryRunner } from "typeorm";
import { MigrationInterface, QueryRunner } from 'typeorm';

export class ImplementFourFeatures1780262000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// Add new fields to User entity
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "displayName" varchar(100)`);
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "email" varchar(255)`);
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "emailVerified" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "avatarUrl" varchar(500)`);
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "bio" text`);
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "preferredAsset" varchar(20) NOT NULL DEFAULT 'XLM'`);
await queryRunner.query(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_users_email" ON "users" ("email") WHERE "email" IS NOT NULL`);

// Create EmailVerification table
await queryRunner.query(`
public async up(queryRunner: QueryRunner): Promise<void> {
// Add new fields to User entity
await queryRunner.query(
`ALTER TABLE "users" ADD COLUMN "displayName" varchar(100)`,
);
await queryRunner.query(
`ALTER TABLE "users" ADD COLUMN "email" varchar(255)`,
);
await queryRunner.query(
`ALTER TABLE "users" ADD COLUMN "emailVerified" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "users" ADD COLUMN "avatarUrl" varchar(500)`,
);
await queryRunner.query(`ALTER TABLE "users" ADD COLUMN "bio" text`);
await queryRunner.query(
`ALTER TABLE "users" ADD COLUMN "preferredAsset" varchar(20) NOT NULL DEFAULT 'XLM'`,
);
await queryRunner.query(
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_users_email" ON "users" ("email") WHERE "email" IS NOT NULL`,
);

// Create EmailVerification table
await queryRunner.query(`
CREATE TABLE "email_verifications" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"userId" uuid NOT NULL,
Expand All @@ -24,34 +36,48 @@ export class ImplementFourFeatures1780262000000 implements MigrationInterface {
CONSTRAINT "UQ_email_verifications_token" UNIQUE ("token")
)
`);
await queryRunner.query(`ALTER TABLE "email_verifications" ADD CONSTRAINT "FK_email_verifications_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE`);

// Add new fields to Escrow entity
await queryRunner.query(`ALTER TABLE "escrows" ADD COLUMN "releasedAmount" numeric(18,7) NOT NULL DEFAULT 0`);

// Add new fields to Condition entity
await queryRunner.query(`ALTER TABLE "escrow_conditions" ADD COLUMN "isReleased" boolean NOT NULL DEFAULT false`);
await queryRunner.query(`ALTER TABLE "escrow_conditions" ADD COLUMN "releasedAt" TIMESTAMP`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Drop new columns from Condition entity
await queryRunner.query(`ALTER TABLE "escrow_conditions" DROP COLUMN "releasedAt"`);
await queryRunner.query(`ALTER TABLE "escrow_conditions" DROP COLUMN "isReleased"`);

// Drop new columns from Escrow entity
await queryRunner.query(`ALTER TABLE "escrows" DROP COLUMN "releasedAmount"`);

// Drop EmailVerification table
await queryRunner.query(`DROP TABLE "email_verifications"`);

// Drop new columns from User entity
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_users_email"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "preferredAsset"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "bio"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarUrl"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "emailVerified"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "email"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "displayName"`);
}
await queryRunner.query(
`ALTER TABLE "email_verifications" ADD CONSTRAINT "FK_email_verifications_userId" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE`,
);

// Add new fields to Escrow entity
await queryRunner.query(
`ALTER TABLE "escrows" ADD COLUMN "releasedAmount" numeric(18,7) NOT NULL DEFAULT 0`,
);

// Add new fields to Condition entity
await queryRunner.query(
`ALTER TABLE "escrow_conditions" ADD COLUMN "isReleased" boolean NOT NULL DEFAULT false`,
);
await queryRunner.query(
`ALTER TABLE "escrow_conditions" ADD COLUMN "releasedAt" TIMESTAMP`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
// Drop new columns from Condition entity
await queryRunner.query(
`ALTER TABLE "escrow_conditions" DROP COLUMN "releasedAt"`,
);
await queryRunner.query(
`ALTER TABLE "escrow_conditions" DROP COLUMN "isReleased"`,
);

// Drop new columns from Escrow entity
await queryRunner.query(
`ALTER TABLE "escrows" DROP COLUMN "releasedAmount"`,
);

// Drop EmailVerification table
await queryRunner.query(`DROP TABLE "email_verifications"`);

// Drop new columns from User entity
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_users_email"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "preferredAsset"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "bio"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarUrl"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "emailVerified"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "email"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "displayName"`);
}
}
15 changes: 15 additions & 0 deletions apps/backend/src/migrations/1780300000000-AddRespondedAtToParty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddRespondedAtToParty1780300000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "escrow_parties" ADD COLUMN "respondedAt" datetime`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "escrow_parties" DROP COLUMN "respondedAt"`,
);
}
}
9 changes: 7 additions & 2 deletions apps/backend/src/modules/auth/controllers/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,10 @@ export class AuthController {
@Req() req: Request & { user: { userId: string } },
@Body() updateProfileDto: UpdateProfileDto,
) {
const user = await this.authService.updateProfile(req.user.userId, updateProfileDto);
const user = await this.authService.updateProfile(
req.user.userId,
updateProfileDto,
);
return {
id: user.id,
walletAddress: user.walletAddress,
Expand Down Expand Up @@ -109,7 +112,9 @@ export class AuthController {
@Post('profile/verify-email')
@UseGuards(AuthGuard)
@HttpCode(HttpStatus.OK)
async sendEmailVerification(@Req() req: Request & { user: { userId: string } }) {
async sendEmailVerification(
@Req() req: Request & { user: { userId: string } },
) {
await this.authService.sendEmailVerification(req.user.userId);
return { message: 'Verification email sent' };
}
Expand Down
8 changes: 7 additions & 1 deletion apps/backend/src/modules/auth/dto/profile.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { IsBoolean, IsEmail, IsOptional, IsString, MaxLength } from 'class-validator';
import {
IsBoolean,
IsEmail,
IsOptional,
IsString,
MaxLength,
} from 'class-validator';

export class UpdateProfileDto {
@IsOptional()
Expand Down
15 changes: 7 additions & 8 deletions apps/backend/src/modules/auth/services/auth.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ describe('AuthService', () => {
userService = module.get(UserService);
jwtService = module.get(JwtService);
configService = module.get(ConfigService);
emailVerificationRepository = module.get(getRepositoryToken(EmailVerification));
emailVerificationRepository = module.get(
getRepositoryToken(EmailVerification),
);
ipfsService = module.get(IpfsService);
});

Expand Down Expand Up @@ -128,20 +130,17 @@ describe('AuthService', () => {
it('should throw UnauthorizedException if user not found', async () => {
userService.findByWalletAddress.mockResolvedValue(null);

await expect(
service.verifySignature('sig', 'GD...123'),
).rejects.toThrow(UnauthorizedException);
await expect(service.verifySignature('sig', 'GD...123')).rejects.toThrow(
UnauthorizedException,
);
});

it('should return tokens on valid signature', async () => {
userService.findByWalletAddress.mockResolvedValue(mockUser as any);
userService.update.mockResolvedValue(mockUser as any);
userService.createRefreshToken.mockResolvedValue({} as any);

const result = await service.verifySignature(
'sig',
'GD...123',
);
const result = await service.verifySignature('sig', 'GD...123');

expect(result).toHaveProperty('accessToken');
expect(result).toHaveProperty('refreshToken');
Expand Down
21 changes: 17 additions & 4 deletions apps/backend/src/modules/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Injectable, UnauthorizedException, BadRequestException } from '@nestjs/common';
import {
Injectable,
UnauthorizedException,
BadRequestException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import * as crypto from 'crypto';
Expand Down Expand Up @@ -127,7 +131,10 @@ export class AuthService {
return user;
}

async updateProfile(userId: string, updateProfileDto: UpdateProfileDto): Promise<User> {
async updateProfile(
userId: string,
updateProfileDto: UpdateProfileDto,
): Promise<User> {
const user = await this.userService.findById(userId);
if (!user) {
throw new UnauthorizedException('User not found');
Expand All @@ -141,13 +148,19 @@ export class AuthService {
return this.userService.update(userId, updateProfileDto);
}

async uploadAvatar(userId: string, file: { buffer: Buffer; originalname: string }): Promise<User> {
async uploadAvatar(
userId: string,
file: { buffer: Buffer; originalname: string },
): Promise<User> {
const user = await this.userService.findById(userId);
if (!user) {
throw new UnauthorizedException('User not found');
}

const cid = await this.ipfsService.uploadFile(file.buffer, file.originalname);
const cid = await this.ipfsService.uploadFile(
file.buffer,
file.originalname,
);
const avatarUrl = this.ipfsService.getGatewayUrl(cid);

return this.userService.update(userId, { avatarUrl });
Expand Down
45 changes: 45 additions & 0 deletions apps/backend/src/modules/escrow/controllers/escrow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,17 @@ export class EscrowController {
return this.escrowService.findOverview(userId, query);
}

@Get('pending-invitations')
@ApiOperation({
summary:
'List escrows where the authenticated user has a pending party invitation',
})
async getPendingInvitations(@Request() req: AuthenticatedRequest) {
return this.escrowService.getPendingInvitations(
this.getAuthenticatedUserId(req),
);
}

@Get(':id')
@UseGuards(EscrowAccessGuard)
async findOne(@Param('id') id: string) {
Expand Down Expand Up @@ -269,6 +280,40 @@ export class EscrowController {
);
}

@Post(':id/parties/:partyId/accept')
@UseGuards(EscrowAccessGuard)
@ApiOperation({ summary: 'Accept a party invitation for an escrow' })
async acceptPartyInvitation(
@Param('id') escrowId: string,
@Param('partyId') partyId: string,
@Request() req: AuthenticatedRequest,
) {
const ipAddress = req.ip || req.socket?.remoteAddress;
return this.escrowService.acceptPartyInvitation(
escrowId,
partyId,
this.getAuthenticatedUserId(req),
ipAddress,
);
}

@Post(':id/parties/:partyId/reject')
@UseGuards(EscrowAccessGuard)
@ApiOperation({ summary: 'Reject a party invitation for an escrow' })
async rejectPartyInvitation(
@Param('id') escrowId: string,
@Param('partyId') partyId: string,
@Request() req: AuthenticatedRequest,
) {
const ipAddress = req.ip || req.socket?.remoteAddress;
return this.escrowService.rejectPartyInvitation(
escrowId,
partyId,
this.getAuthenticatedUserId(req),
ipAddress,
);
}

/**
* POST /escrows/:id/dispute
* File a dispute against an active escrow. Only a buyer or seller party may call this.
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/modules/escrow/entities/party.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export class Party {
})
status: PartyStatus;

@Column({ type: 'datetime', nullable: true })
respondedAt: Date | null;

@CreateDateColumn()
createdAt: Date;
}
2 changes: 2 additions & 0 deletions apps/backend/src/modules/escrow/escrow.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { AuthModule } from '../auth/auth.module';
import { EscrowStellarIntegrationService } from './services/escrow-stellar-integration.service';
import { WebhookModule } from '../webhook/webhook.module';
import { IpfsModule } from '../ipfs/ipfs.module';
import { NotificationsModule } from '../../notifications/notifications.module';
import { User } from '../user/entities/user.entity';
import { AllowedAsset } from '../assets/entities/allowed-asset.entity';
import { EscrowLifecycleService } from './escrow-lifecycle.service';
Expand All @@ -37,6 +38,7 @@ import { EscrowQueryService } from './escrow-query.service';
AuthModule,
WebhookModule,
IpfsModule,
NotificationsModule,
],
controllers: [EscrowController, EscrowSchedulerController, EventsController],
providers: [
Expand Down
Loading
Loading