All files / src/lib/services reset-token-service.ts

97.62% Statements 41/42
100% Branches 2/2
100% Functions 11/11
97.62% Lines 41/42

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 12763x 63x 63x   63x 63x               63x           63x                     159x 159x 159x       5x 5x 4x 4x   1x         3x 3x 3x   11x   11x   11x     3x               19x 19x 16x 14x     3x   2x       11x               28x 28x                 8x 8x       20x 20x               32x 32x 32x 32x                 32x       63x  
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import { URL } from 'url';
import { Logger } from '../logger';
import UsedTokenError from '../error/used-token-error';
import InvalidTokenError from '../error/invalid-token-error';
import { IUnleashConfig } from '../types/option';
import { IUnleashStores } from '../types/stores';
import {
    IResetQuery,
    IResetToken,
    IResetTokenStore,
} from '../types/stores/reset-token-store';
import { hoursToMilliseconds } from 'date-fns';
 
interface IInviteLinks {
    [key: string]: string;
}
 
export default class ResetTokenService {
    private store: IResetTokenStore;
 
    private logger: Logger;
 
    private readonly unleashBase: string;
 
    constructor(
        { resetTokenStore }: Pick<IUnleashStores, 'resetTokenStore'>,
        { getLogger, server }: Pick<IUnleashConfig, 'getLogger' | 'server'>,
    ) {
        this.store = resetTokenStore;
        this.logger = getLogger('/services/reset-token-service.ts');
        this.unleashBase = server.unleashUrl;
    }
 
    async useAccessToken(token: IResetQuery): Promise<boolean> {
        try {
            await this.isValid(token.token);
            await this.store.useToken(token);
            return true;
        } catch (e) {
            return false;
        }
    }
 
    async getActiveInvitations(): Promise<IInviteLinks> {
        try {
            const tokens = await this.store.getActiveTokens();
            const links = tokens.reduce((acc, token) => {
                const inviteLink =
                    this.getExistingInvitationUrl(token).toString();
 
                acc[token.userId] = inviteLink;
 
                return acc;
            }, {});
 
            return links;
        } catch (e) {
            return {};
        }
    }
 
    async isValid(token: string): Promise<IResetToken> {
        let t;
        try {
            t = await this.store.getActive(token);
            if (!t.usedAt) {
                return t;
            }
        } catch (e) {
            throw new InvalidTokenError();
        }
        throw new UsedTokenError(t.usedAt);
    }
 
    private getExistingInvitationUrl(token: IResetToken) {
        return new URL(`${this.unleashBase}/new-user?token=${token.token}`);
    }
 
    private async createResetUrl(
        forUser: number,
        creator: string,
        path: string,
    ): Promise<URL> {
        const token = await this.createToken(forUser, creator);
        return Promise.resolve(
            new URL(`${this.unleashBase}${path}?token=${token.token}`),
        );
    }
 
    async createResetPasswordUrl(
        forUser: number,
        creator: string,
    ): Promise<URL> {
        const path = '/reset-password';
        return this.createResetUrl(forUser, creator, path);
    }
 
    async createNewUserUrl(forUser: number, creator: string): Promise<URL> {
        const path = '/new-user';
        return this.createResetUrl(forUser, creator, path);
    }
 
    async createToken(
        tokenUser: number,
        creator: string,
        expiryDelta: number = hoursToMilliseconds(24),
    ): Promise<IResetToken> {
        const token = await this.generateToken();
        const expiry = new Date(Date.now() + expiryDelta);
        await this.store.expireExistingTokensForUser(tokenUser);
        return this.store.insert({
            reset_token: token,
            user_id: tokenUser,
            expires_at: expiry,
            created_by: creator,
        });
    }
 
    private generateToken(): Promise<string> {
        return bcrypt.hash(crypto.randomBytes(32).toString(), 10);
    }
}
 
module.exports = ResetTokenService;