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
14 changes: 12 additions & 2 deletions src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ export class Configuration {
expiresIn: process.env.JWT_EXPIRES_IN ?? 172800,
},
},
company: {
signOptions: {
expiresIn: process.env.JWT_EXPIRES_IN_COMPANY ?? 30,
},
},
challenge: {
expiresIn: +process.env.CHALLENGE_EXPIRES_IN ?? 10,
},
signMessage:
'By_signing_this_message,_you_confirm_that_you_are_the_sole_owner_of_the_provided_DeFiChain_address_and_are_in_possession_of_its_private_key._Your_ID:_',
signMessageWallet:
Expand Down Expand Up @@ -119,8 +127,10 @@ export class Configuration {
apiKey: process.env.FIXER_API_KEY,
};

lock = {
apiKey: process.env.LOCK_API_KEY,
externalKycServices = {
'LOCK.space': {
apiKey: process.env.LOCK_API_KEY,
},
};

mail: MailOptions = {
Expand Down
5 changes: 5 additions & 0 deletions src/user/models/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ export class AuthController {
signInCompany(@Body() credentials: AuthCredentialsDto): Promise<{ accessToken: string }> {
return this.authService.companySignIn(credentials);
}

@Post('company/challenge')
companyChallenge(@Query('address') address: string): Promise<{ challenge: string }> {
return this.authService.getCompanyChallenge(address);
}
}
80 changes: 60 additions & 20 deletions src/user/models/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,19 @@ import { Blockchain } from 'src/blockchain/shared/enums/blockchain.enum';
import { WalletRepository } from '../wallet/wallet.repository';
import { Wallet } from '../wallet/wallet.entity';
import { UserRole } from 'src/shared/auth/user-role.enum';
import { Util } from 'src/shared/util';
import { Interval } from '@nestjs/schedule';
import { randomUUID } from 'crypto';

export interface ChallengeData {
created: Date;
challenge: string;
}

@Injectable()
export class AuthService {
private challengeList: Map<string, ChallengeData> = new Map<string, ChallengeData>();

constructor(
private readonly userService: UserService,
private readonly userRepo: UserRepository,
Expand All @@ -33,13 +43,24 @@ export class AuthService {
private readonly refService: RefService,
) {}

@Interval(90000)
checkChallengeList() {
for (const [key, challenge] of this.challengeList.entries()) {
if (!this.isChallengeValid(challenge)) {
this.challengeList.delete(key);
}
}
}

// --- AUTH METHODS --- //
async signUp(dto: CreateUserDto, userIp: string): Promise<{ accessToken: string }> {
const existingUser = await this.userRepo.getByAddress(dto.address);
if (existingUser) {
throw new ConflictException('User already exists');
}

if (!this.verifySignature(dto.address, dto.signature)) {
const { message } = this.getSignMessage(dto.address);
if (!this.verifySignature(message, dto.address, dto.signature)) {
throw new BadRequestException('Invalid signature');
}

Expand All @@ -56,7 +77,8 @@ export class AuthService {
const user = await this.userRepo.getByAddress(address);
if (!user) throw new NotFoundException('User not found');

const credentialsValid = this.verifySignature(address, signature);
const { message } = this.getSignMessage(address);
const credentialsValid = this.verifySignature(message, address, signature);
if (!credentialsValid) throw new UnauthorizedException('Invalid credentials');

// TODO: temporary code to update old wallet signatures
Expand All @@ -67,17 +89,36 @@ export class AuthService {
return { accessToken: this.generateUserToken(user) };
}

async companySignIn({ address, signature }: AuthCredentialsDto): Promise<{ accessToken: string }> {
const wallet = await this.walletRepo.getByAddress(address);
async companySignIn(dto: AuthCredentialsDto): Promise<{ accessToken: string }> {
const wallet = await this.walletRepo.findOne({ where: { address: dto.address } });
if (!wallet || !wallet.isKycClient) throw new NotFoundException('Wallet not found');

// TODO add challenge response
const credentialsValid = this.verifyCompanySignature(address, signature);
const credentialsValid = this.verifyCompanySignature(dto);
if (!credentialsValid) throw new UnauthorizedException('Invalid credentials');

return { accessToken: this.generateCompanyToken(wallet) };
}

async getCompanyChallenge(address: string): Promise<{ challenge: string }> {
const wallet = await this.walletRepo.findOne({ where: { address: address } });
if (!wallet || !wallet.isKycClient) throw new BadRequestException('Wallet not found/invalid');

const challenge = randomUUID();

this.challengeList.set(address, { created: new Date(), challenge: challenge });

return { challenge: challenge };
}

async changeUser(id: number, changeUser: LinkedUserInDto): Promise<{ accessToken: string }> {
const user = await this.getLinkedUser(id, changeUser.address);
if (!user) throw new NotFoundException('User not found');
if (user.stakingBalance > 0) throw new ForbiddenException('Change user not allowed');
return { accessToken: this.generateUserToken(user) };
}

// --- HELPER METHODS --- //

getSignMessage(address: string): { message: string; blockchains: Blockchain[] } {
const blockchains = this.cryptoService.getBlockchainsBasedOn(address);
return {
Expand All @@ -94,13 +135,6 @@ export class AuthService {
};
}

async changeUser(id: number, changeUser: LinkedUserInDto): Promise<{ accessToken: string }> {
const user = await this.getLinkedUser(id, changeUser.address);
if (!user) throw new NotFoundException('User not found');
if (user.stakingBalance > 0) throw new ForbiddenException('Change user not allowed');
return { accessToken: this.generateUserToken(user) };
}

private async getLinkedUser(id: number, address: string): Promise<User> {
return this.userRepo
.createQueryBuilder('user')
Expand All @@ -112,14 +146,16 @@ export class AuthService {
.getRawOne<User>();
}

private verifySignature(address: string, signature: string): boolean {
const signatureMessage = this.getSignMessage(address);
return this.cryptoService.verifySignature(signatureMessage.message, address, signature);
private verifySignature(message: string, address: string, signature: string): boolean {
return this.cryptoService.verifySignature(message, address, signature);
}

private verifyCompanySignature(address: string, signature: string): boolean {
const signatureMessage = this.getCompanySignMessage(address);
return this.cryptoService.verifySignature(signatureMessage.message, address, signature);
private verifyCompanySignature(dto: AuthCredentialsDto): boolean {
const challengeData = this.challengeList.get(dto.address);
if (!this.isChallengeValid(challengeData)) throw new UnauthorizedException('Challenge invalid');
this.challengeList.delete(dto.address);

return this.verifySignature(challengeData.challenge, dto.address, dto.signature);
}

private generateUserToken(user: User): string {
Expand All @@ -138,6 +174,10 @@ export class AuthService {
address: wallet.address,
role: UserRole.KYC_CLIENT_COMPANY,
};
return this.jwtService.sign(payload);
return this.jwtService.sign(payload, { expiresIn: Config.auth.company.signOptions.expiresIn });
}

private isChallengeValid(challenge: ChallengeData): boolean {
return challenge && Util.secondsDiff(challenge.created, new Date()) <= Config.auth.challenge.expiresIn;
}
}
2 changes: 1 addition & 1 deletion src/user/models/ident/ident.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class IdentService {
spiderData: { identIdentificationIds: Like(`%${result?.identificationprocess?.id}%`) },
},
],
relations: ['spiderData'],
relations: ['spiderData', 'users', 'users.wallet'],
});

if (!user) {
Expand Down
46 changes: 27 additions & 19 deletions src/user/models/kyc/kyc-process.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ export class KycProcessService {

// --- GENERAL METHODS --- //
async startKycProcess(userData: UserData): Promise<UserData> {
return await this.goToStatus(userData, KycStatus.CHATBOT);
const lockUser = userData.users.find((e) => e.wallet.name === 'LOCK.space');
Comment thread
Yannick1712 marked this conversation as resolved.
return await this.goToStatus(userData, lockUser ? KycStatus.ONLINE_ID : KycStatus.CHATBOT);
}

async checkKycProcess(userData: UserData): Promise<UserData> {
Expand Down Expand Up @@ -56,7 +57,7 @@ export class KycProcessService {
userData.spiderData = await this.updateSpiderData(userData, initiateData);
}

if (status === KycStatus.MANUAL) {
if (status === KycStatus.MANUAL && !userData.hasExternalUser) {
if (userData.mail) {
await this.notificationService.sendMail({
type: MailType.USER,
Expand Down Expand Up @@ -95,24 +96,29 @@ export class KycProcessService {
if (userData.kycStatus === KycStatus.ONLINE_ID) {
userData = await this.goToStatus(userData, KycStatus.VIDEO_ID);

await this.notificationService
.sendMail({
type: MailType.USER,
input: {
userData,
translationKey: 'mail.kyc.failed',
translationParams: {
url: `${Config.payment.url}/kyc?code=${userData.kycHash}`,
if (!userData.hasExternalUser) {
await this.notificationService
.sendMail({
type: MailType.USER,
input: {
userData,
translationKey: 'mail.kyc.failed',
translationParams: {
url: `${Config.payment.url}/kyc?code=${userData.kycHash}`,
},
},
},
})
.catch(() => null);
})
.catch(() => null);
}

return userData;
}

// notify support
await this.notificationService.sendMail({ type: MailType.KYC_SUPPORT, input: { userData } });

//kyc Webhook external Services
await this.kycWebhookService.kycFailed(userData, 'Kyc step failed');
return this.updateKycState(userData, KycState.FAILED);
}

Expand Down Expand Up @@ -167,12 +173,14 @@ export class KycProcessService {
async identCompleted(userData: UserData, result: IdentResultDto): Promise<UserData> {
userData = await this.storeIdentResult(userData, result);

await this.notificationService
.sendMail({
type: MailType.USER,
input: { userData, translationKey: 'mail.kyc.ident', translationParams: {} },
})
.catch(() => null);
if (!userData.hasExternalUser) {
await this.notificationService
.sendMail({
type: MailType.USER,
input: { userData, translationKey: 'mail.kyc.ident', translationParams: {} },
})
.catch(() => null);
}

return await this.goToStatus(userData, KycStatus.CHECK);
}
Expand Down
49 changes: 31 additions & 18 deletions src/user/models/kyc/kyc-webhook.service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Injectable } from '@nestjs/common';
import { HttpService } from 'src/shared/services/http.service';
import { WalletRepository } from '../wallet/wallet.repository';
import { KycCompleted, UserData } from '../user-data/user-data.entity';
import { Config } from 'src/config/config';
import { KycCompleted, KycStatus, UserData } from '../user-data/user-data.entity';
import { UserRepository } from '../user/user.repository';
import { SpiderDataRepository } from '../spider-data/spider-data.repository';
import { WalletService } from '../wallet/wallet.service';

export enum KycWebhookStatus {
NA = 'NA',
LIGHT = 'Light',
FULL = 'Full',
REJECTED = 'Rejected',
}

export enum KycWebhookResult {
Expand Down Expand Up @@ -37,7 +39,12 @@ export class KycWebhookDto {

@Injectable()
export class KycWebhookService {
constructor(private readonly http: HttpService, private readonly walletRepo: WalletRepository) {}
constructor(
private readonly http: HttpService,
private readonly walletService: WalletService,
private readonly userRepo: UserRepository,
private readonly spiderRepo: SpiderDataRepository,
) {}

async kycChanged(userData: UserData): Promise<void> {
await this.triggerWebhook(userData, KycWebhookResult.STATUS_CHANGED);
Expand All @@ -48,19 +55,13 @@ export class KycWebhookService {
}

private async triggerWebhook(userData: UserData, result: KycWebhookResult, reason?: string): Promise<void> {
if (!userData.users) {
console.info(`Tried to trigger webhook for user ${userData.id}, but users were not loaded`);
return;
}
userData.users = await this.userRepo.find({ where: { userData: { id: userData.id } }, relations: ['wallet'] });

for (const user of userData.users) {
try {
if (!user.wallet?.id) {
console.info(`Tried to trigger webhook for user ${userData.id}, but wallet were not loaded`);
continue;
}
const walletUser = await this.walletRepo.findOne({ where: { id: user.wallet.id } });
if (!walletUser || !walletUser.isKycClient || !walletUser.apiUrl) continue;
if (!user.wallet.isKycClient || !user.wallet.apiUrl) continue;

const spiderData = await this.spiderRepo.findOne({ where: { userData: { id: userData.id } } });

const data: KycWebhookDto = {
id: user.address,
Expand All @@ -74,19 +75,31 @@ export class KycWebhookService {
city: userData.location,
zip: userData.zip,
phone: userData.phone,
//TODO change for KYC Update v2
kycStatus: KycCompleted(userData.kycStatus) ? KycWebhookStatus.FULL : KycWebhookStatus.NA,
kycStatus: this.getKycWebhookStatus(userData.kycStatus, spiderData?.chatbotResult),
kycHash: userData.kycHash,
},
reason: reason,
};

await this.http.post(`${walletUser.apiUrl}/kyc/update`, data, {
headers: { 'x-api-key': Config.lock.apiKey },
const apiKey = this.walletService.getApiKeyInternal(user.wallet.name);
if (!apiKey) throw new Error(`ApiKey for wallet ${user.wallet.name} not available`);

await this.http.post(`${user.wallet.apiUrl}/kyc/update`, data, {
headers: { 'x-api-key': apiKey },
});
} catch (error) {
console.error(`Exception during KYC webhook (${result}) for user ${userData.id}:`, error);
}
}
}

getKycWebhookStatus(kycStatus: KycStatus, chatbotResult: string): KycWebhookStatus {
if (KycCompleted(kycStatus)) {
return chatbotResult ? KycWebhookStatus.FULL : KycWebhookStatus.LIGHT;
} else if (kycStatus === KycStatus.REJECTED) {
return KycWebhookStatus.REJECTED;
} else {
return KycWebhookStatus.NA;
}
}
}
Loading