Skip to content
This repository has been archived by the owner on Apr 19, 2023. It is now read-only.

Commit

Permalink
♻️ Update services
Browse files Browse the repository at this point in the history
  • Loading branch information
AnandChowdhary committed Jan 9, 2021
1 parent ab93f92 commit 54deaf5
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 64 deletions.
25 changes: 13 additions & 12 deletions src/providers/elasticsearch/elasticsearch.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,26 @@ import { Configuration } from '../../config/configuration.interface';
export class ElasticSearchService {
private logger = new Logger(ElasticSearchService.name);
private queue = new PQueue({ concurrency: 1 });
private elasticSearchConfig = this.configService.get<
Configuration['elasticSearch']
>('elasticSearch');
client?: Client;

constructor(private configService: ConfigService) {
if (this.elasticSearchConfig.aws?.accessKeyId) {
const config = this.configService.get<Configuration['elasticSearch']>(
'elasticSearch',
);
if (config.aws?.accessKeyId) {
AWS.config.update({
accessKeyId: this.elasticSearchConfig.aws.accessKeyId,
secretAccessKey: this.elasticSearchConfig.aws.secretAccessKey,
region: this.elasticSearchConfig.aws.region,
accessKeyId: config.aws.accessKeyId,
secretAccessKey: config.aws.secretAccessKey,
region: config.aws.region,
});
this.client = new Client({
...createAwsElasticsearchConnector(AWS.config),
node: this.elasticSearchConfig.node,
node: config.node,
});
} else if (this.elasticSearchConfig.node)
} else if (config.node)
this.client = new Client({
auth: this.elasticSearchConfig.auth,
node: this.elasticSearchConfig.node,
auth: config.auth,
node: config.node,
});
else this.logger.warn('ElasticSearch tracking is not enabled');
}
Expand All @@ -42,7 +42,8 @@ export class ElasticSearchService {
this.queue
.add(() =>
pRetry(() => this.indexRecord(index, record, params), {
retries: this.elasticSearchConfig.retries,
retries:
this.configService.get<number>('elasticSearch.retries') ?? 3,
onFailedAttempt: (error) => {
this.logger.error(
`Indexing record failed, retrying (${error.retriesLeft} attempts left)`,
Expand Down
5 changes: 1 addition & 4 deletions src/providers/geolocation/geolocation.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ConfigService } from '@nestjs/config';
import geolite2 from 'geolite2-redist';
import maxmind, { CityResponse, Reader } from 'maxmind';
import QuickLRU from 'quick-lru';
import { Configuration } from '../../config/configuration.interface';

@Injectable()
export class GeolocationService implements OnModuleDestroy {
Expand All @@ -12,9 +11,7 @@ export class GeolocationService implements OnModuleDestroy {
private lookup: Reader<CityResponse> | null = null;
private lru = new QuickLRU<string, Partial<CityResponse>>({
maxSize:
this.configService.get<Configuration['caching']['geolocationLruSize']>(
'caching.geolocationLruSize',
) ?? 100,
this.configService.get<number>('caching.geolocationLruSize') ?? 100,
});

onModuleDestroy() {
Expand Down
19 changes: 9 additions & 10 deletions src/providers/mail/mail.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,22 @@ import { MailOptions } from './mail.interface';
export class MailService {
private readonly logger = new Logger(MailService.name);
private transport: Mail;
private emailConfig = this.configService.get<Configuration['email']>('email');
private config: Configuration['email'];
private queue = new PQueue({ concurrency: 1 });
private readTemplate = mem(this.readTemplateUnmemoized);

constructor(private configService: ConfigService) {
if (this.emailConfig.ses?.accessKeyId)
this.config = this.configService.get<Configuration['email']>('email');
if (this.config.ses?.accessKeyId)
this.transport = nodemailer.createTransport({
SES: new SES({
apiVersion: '2010-12-01',
accessKeyId: this.emailConfig.ses.accessKeyId,
secretAccessKey: this.emailConfig.ses.secretAccessKey,
region: this.emailConfig.ses.region,
accessKeyId: this.config.ses.accessKeyId,
secretAccessKey: this.config.ses.secretAccessKey,
region: this.config.ses.region,
}),
} as SESTransport.Options);
else
this.transport = nodemailer.createTransport(this.emailConfig.transport);
else this.transport = nodemailer.createTransport(this.config.transport);
}

send(options: Mail.Options & MailOptions) {
Expand All @@ -43,11 +43,10 @@ export class MailService {
this.sendMail({
...options,
from:
options.from ??
`"${this.emailConfig.name}" <${this.emailConfig.from}>`,
options.from ?? `"${this.config.name}" <${this.config.from}>`,
}),
{
retries: this.emailConfig.retries,
retries: this.configService.get<number>('email.retries') ?? 3,
onFailedAttempt: (error) => {
this.logger.error(
`Mail to ${options.to} failed, retrying (${error.retriesLeft} attempts left)`,
Expand Down
2 changes: 2 additions & 0 deletions src/providers/prisma/prisma.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export class PrismaService
/** Delete sensitive keys from an object */
expose<T>(item: T): Expose<T> {
if (!item) return {} as T;
if (((item as any) as Partial<User>).password)
(item as any).hasPassword = true;
delete ((item as any) as Partial<User>).password;
delete ((item as any) as Partial<User>).twoFactorSecret;
delete ((item as any) as Partial<Session>).token;
Expand Down
66 changes: 60 additions & 6 deletions src/providers/s3/s3.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import AWS from 'aws-sdk';
import { AWSError, S3 } from 'aws-sdk';
import { Configuration } from '../../config/configuration.interface';

@Injectable()
export class S3Service {
client?: AWS.S3;
client?: S3;
private logger = new Logger(S3Service.name);

constructor(private configService: ConfigService) {
const config = this.configService.get<Configuration['s3']>('s3');
if (config.accessKeyId)
this.client = new AWS.S3({
this.client = new S3({
apiVersion: '2006-03-01',
accessKeyId: config.accessKeyId,
secretAccessKey: config.secretAccessKey,
Expand All @@ -20,19 +20,73 @@ export class S3Service {
else this.logger.warn('No S3 API key set');
}

/** Get a signed URL to access an S3 object for 5 minutes */
signedUrl(bucket: string, key: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
this.client.getSignedUrl(
'getObject',
{
Bucket: bucket,
Key: key,
Expires: 300,
},
(error: AWSError, data: string) => {
if (error) return reject(error);
resolve(data);
},
);
});
}

/** Get a policy to upload to S3 directly */
postPolicy(bucket: string, key: string): Promise<S3.PresignedPost> {
return new Promise<S3.PresignedPost>((resolve, reject) => {
this.client.createPresignedPost(
{
Bucket: bucket,
Fields: {
key,
},
Expires: 300,
},
(error: AWSError, data: S3.PresignedPost) => {
if (error) return reject(error);
resolve(data);
},
);
});
}

upload(
name: string,
body: Buffer,
bucket?: string,
): Promise<AWS.S3.ManagedUpload.SendData> {
return new Promise((resolve, reject) => {
publicRead?: true,
): Promise<S3.ManagedUpload.SendData> {
return new Promise<S3.ManagedUpload.SendData>((resolve, reject) => {
this.client.upload(
{
Bucket: bucket,
Key: name,
Body: body,
ACL: publicRead ? 'public-read' : undefined,
},
(error: AWSError, data: S3.ManagedUpload.SendData) => {
if (error) return reject(error);
resolve(data);
},
);
});
}

get(bucket: string, name: string): Promise<S3.GetObjectOutput> {
return new Promise((resolve, reject) => {
this.client.getObject(
{
Bucket: bucket,
Key: name,
},
(error: any, data: any) => {
(error: AWSError, data: S3.Types.GetObjectOutput) => {
if (error) return reject(error);
resolve(data);
},
Expand Down
14 changes: 7 additions & 7 deletions src/providers/slack/slack.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,23 @@ import { Configuration } from '../../config/configuration.interface';
@Injectable()
export class SlackService {
client?: WebClient;
private slackConfig = this.configService.get<Configuration['slack']>('slack');
private logger = new Logger(SlackService.name);
private queue = new PQueue({ concurrency: 1 });

constructor(private configService: ConfigService) {
if (this.slackConfig.token)
this.client = new WebClient(this.slackConfig.token, {
slackApiUrl: this.slackConfig.slackApiUrl,
rejectRateLimitedCalls: this.slackConfig.rejectRateLimitedCalls,
const config = this.configService.get<Configuration['slack']>('slack');
if (config.token)
this.client = new WebClient(config.token, {
slackApiUrl: config.slackApiUrl,
rejectRateLimitedCalls: config.rejectRateLimitedCalls,
});
}

send(options: ChatPostMessageArguments) {
this.queue
.add(() =>
pRetry(() => this.sendMessage(options), {
retries: this.slackConfig.retries,
retries: this.configService.get<number>('slack.retries') ?? 3,
onFailedAttempt: (error) => {
this.logger.error(
`Message to ${options.channel} failed, retrying (${error.retriesLeft} attempts left)`,
Expand All @@ -45,7 +45,7 @@ export class SlackService {
this.queue
.add(() =>
pRetry(() => this.sendMessageToChannel(channelName, text), {
retries: this.slackConfig.retries,
retries: this.configService.get<number>('slack.retries') ?? 3,
onFailedAttempt: (error) => {
this.logger.error(
`Message to ${channelName} failed, retrying (${error.retriesLeft} attempts left)`,
Expand Down
93 changes: 73 additions & 20 deletions src/providers/tokens/tokens.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { async as cryptoRandomString } from 'crypto-random-string';
import {
decode,
DecodeOptions,
Expand All @@ -8,52 +9,104 @@ import {
verify,
VerifyOptions,
} from 'jsonwebtoken';
import { Configuration } from 'src/config/configuration.interface';
import { v4 } from 'uuid';
import { INVALID_TOKEN } from '../../errors/errors.constants';

@Injectable()
export class TokensService {
private securityConfig = this.configService.get<Configuration['security']>(
'security',
);

constructor(private configService: ConfigService) {}

/**
* Sign a JWT
* @param subject - Subject
* @param payload - Object payload
* @param expiresIn - Expiry string (vercel/ms)
* @param options - Signing options
*/
signJwt(
jwtType: string,
payload: object,
subject: string,
payload: number | string | object | Buffer,
expiresIn?: string,
options?: SignOptions,
) {
return sign({ ...payload, typ: jwtType }, this.securityConfig.jwtSecret, {
...options,
expiresIn,
issuer: this.securityConfig.issuerDomain,
});
if (typeof payload === 'number') payload = payload.toString();
return sign(
payload,
this.configService.get<string>('security.jwtSecret') ?? '',
{
...options,
subject,
expiresIn,
},
);
}

verify<T>(jwtType: string, token: string, options?: VerifyOptions) {
/**
* Verify and decode a JWT
* @param subject - Subject
* @param token - JWT
* @param options - Verify options
*/
verify<T>(subject: string, token: string, options?: VerifyOptions) {
try {
const result = (verify(
return (verify(
token,
this.securityConfig.jwtSecret,
options,
this.configService.get<string>('security.jwtSecret') ?? '',
{ ...options, subject },
) as any) as T;
if ('typ' in result) {
if ((result as { typ?: string }).typ !== jwtType) throw new Error();
} else throw new Error();
return result;
} catch (error) {
throw new UnauthorizedException(INVALID_TOKEN);
}
}

/**
* Decode a JWT without verifying it
* @deprecated Use verify() instead
* @param token - JWT
* @param options - Decode options
*/
decode<T>(token: string, options?: DecodeOptions) {
return decode(token, options) as T;
}

/**
* Generate a UUID
*/
generateUuid() {
return v4();
}

/**
* Generate a cryptographically strong random string
* @param length - Length of returned string
* @param charactersOrType - Characters or one of the supported types
*/
async generateRandomString(
length = 32,
charactersOrType = 'alphanumeric',
): Promise<string> {
if (
[
'hex',
'base64',
'url-safe',
'numeric',
'distinguishable',
'ascii-printable',
'alphanumeric',
].includes(charactersOrType)
)
return cryptoRandomString({
length,
type: charactersOrType as
| 'hex'
| 'base64'
| 'url-safe'
| 'numeric'
| 'distinguishable'
| 'ascii-printable'
| 'alphanumeric',
});
return cryptoRandomString({ length, characters: charactersOrType });
}
}
Loading

0 comments on commit 54deaf5

Please sign in to comment.