Skip to content

Commit

Permalink
feat: init pay webhook (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
ZigBalthazar authored Sep 1, 2024
1 parent b7c787a commit 341844f
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 2 deletions.
1 change: 1 addition & 0 deletions src/nostr/entities/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './event.entity';
export * from './filter.entity';
export * from './user.entity'
8 changes: 8 additions & 0 deletions src/nostr/entities/user.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class UserEntity {
id: string;
pubkey: string;
admission_fee: number;
created_at: number;
expire_at: number;
status: number;
}
8 changes: 6 additions & 2 deletions src/nostr/nostr.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,30 @@ import { EventController } from './event.controller';
import { NostrController } from './nostr.controller';
import { NostrGateway } from './nostr.gateway';
import { AccessControlPlugin } from './plugins';
import { EventRepository } from './repositories';
import { EventRepository, UserRepository } from './repositories';
import { EventSearchRepository } from './repositories/event-search.repository';
import { EventService } from './services/event.service';
import { MetricService } from './services/metric.service';
import { NostrRelayLogger } from './services/nostr-relay-logger.service';
import { NostrRelayService } from './services/nostr-relay.service';
import { TaskService } from './services/task.service';
import { UserService } from './services/user.service';
import { userController } from './user.contoller';

@Module({
controllers: [NostrController, EventController],
controllers: [NostrController, EventController, userController],
providers: [
EventRepository,
EventSearchRepository,
UserRepository,
NostrGateway,
EventService,
MetricService,
NostrRelayLogger,
AccessControlPlugin,
TaskService,
NostrRelayService,
UserService
],
})
export class NostrModule {}
1 change: 1 addition & 0 deletions src/nostr/repositories/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './event.repository';
export * from './event-search.repository';
export * from './user.repository'
96 changes: 96 additions & 0 deletions src/nostr/repositories/user.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import {
ColumnType,
Kysely,
PostgresDialect,
sql,
} from 'kysely';
import * as pg from 'pg';
import { Config } from 'src/config';
import { EventSearchRepository } from './event-search.repository';

export interface UserDatabase {
users: UserTable;
}

interface UserTable {
pubkey: string;
admission_fee: number;
expire_at: ColumnType<Date, string, string>;
create_date: ColumnType<Date, string, string>;
}

@Injectable()
export class UserRepository {
private readonly db: Kysely<UserDatabase>;

constructor(
private readonly eventSearchRepository: EventSearchRepository,
configService: ConfigService<Config, true>,
) {
const databaseConfig = configService.get('database', { infer: true });

const int8TypeId = 20;
pg.types.setTypeParser(int8TypeId, (val) => parseInt(val, 10));

const dialect = new PostgresDialect({
pool: new pg.Pool({
connectionString: databaseConfig.url,
max: databaseConfig.maxConnections,
}),
});
this.db = new Kysely<UserDatabase>({ dialect });
}

async upsert(pubkey: string, admissionFee: number) {
try {
await this.db.transaction().execute(async (trx) => {
const eventInsertResult = await trx
.insertInto('users')
.values({
pubkey: pubkey,
admission_fee: admissionFee,
expire_at: sql`NOW() + INTERVAL '1 MONTH'`,
create_date: sql`NOW()`,
})
.onConflict((oc) =>
oc.columns(['pubkey']).doUpdateSet({
admission_fee: (eb) => eb.ref('excluded.admission_fee'),
create_date: sql`NOW()`, // Update create_date to current timestamp
expire_at: sql`NOW() + INTERVAL '1 MONTH'`, // Update expire_at to 1 month in the future
}),
)
.executeTakeFirst();

return eventInsertResult;
});

return { isDuplicate: false };
} catch (error) {
if (error.code === '23505') {
// 23505 is unique_violation
return { isDuplicate: true };
}
throw error;
}
}

async findExpireAt(pubkey: string): Promise<Date | null> {
try {
const result = await this.db
.selectFrom('users')
.select('expire_at')
.where('pubkey', '=', pubkey)
.executeTakeFirst();

if (result) {
return result.expire_at;
} else {
return null;
}
} catch (error) {
throw error;
}
}
}
16 changes: 16 additions & 0 deletions src/nostr/services/user.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable } from '@nestjs/common';
import { UserRepository } from '../repositories';

@Injectable()
export class UserService {
constructor(private readonly userRepository: UserRepository) {}

new(pubkey: string, admissionFee: number) {
return this.userRepository.upsert(pubkey, admissionFee);
}

async isEligible(pubkey: string): Promise<boolean> {
const ex = await this.userRepository.findExpireAt(pubkey);
return ex && ex >= new Date() ? true : false;
}
}
54 changes: 54 additions & 0 deletions src/nostr/user.contoller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Body, Controller, Headers, Post, Req, UnauthorizedException } from '@nestjs/common';
import { UserService } from './services/user.service';
import * as crypto from 'crypto';


@Controller('user')
export class userController {
constructor(
private readonly userService: UserService
){}

@Post('webhook')
async webhook(
@Headers('webhook-signature') signature: string,
@Headers('webhook-timestamp') timestamp: string,
@Headers('webhook-id') webhookId: string,
@Body() body: any,
) {
const secret = 'wsec_WJStlX/jEqv9bLVpYPXQZcXZeDsdRIDb'; //just for test

const requestBody = JSON.stringify(body);

const isValid = this.verifySignature(secret, signature, webhookId, timestamp, requestBody);

if (!isValid) {
throw new UnauthorizedException('Invalid webhook signature');
}

console.log('Webhook received and verified:', body);

return { success: true };
}

verifySignature(secret: string, signature: string, webhookId: string, timestamp: string, requestBody: string): boolean {
try {
// Step 2: Prepare the secret string
const cleanedSecret = secret.replace('wsec_', '');
const tempSecret = Buffer.from(cleanedSecret, 'base64');

// Step 3: Prepare the signed_payload string
const signedPayload = `${webhookId}.${timestamp}.${requestBody}`;

// Step 4: Determine the expected signature
const hmac = crypto.createHmac('sha256', tempSecret);
hmac.update(signedPayload, 'utf8');
const expectedSignature = hmac.digest('base64');

// Step 5: Compare the signatures
return signature === expectedSignature;
} catch (error) {
throw new UnauthorizedException('Invalid signature');
}
}
}

0 comments on commit 341844f

Please sign in to comment.