# Self TOTP registration
package Lemonldap::NG::Portal::2F::Register::TOTP;

use strict;
use Mouse;
use JSON qw(from_json to_json);

our $VERSION = '2.0.14';

extends qw(
  Lemonldap::NG::Portal::Main::Plugin
  Lemonldap::NG::Common::TOTP
);

# INITIALIZATION

has prefix   => ( is => 'rw', default => 'totp' );
has template => ( is => 'ro', default => 'totp2fregister' );
has welcome  => ( is => 'ro', default => 'yourNewTotpKey' );
has logo     => ( is => 'rw', default => 'totp.png' );
has ott      => (
    is      => 'rw',
    lazy    => 1,
    default => sub {
        my $ott =
          $_[0]->{p}->loadModule('Lemonldap::NG::Portal::Lib::OneTimeToken');
        my $timeout = $_[0]->{conf}->{sfRegisterTimeout}
          // $_[0]->{conf}->{formTimeout};
        $ott->timeout($timeout);
        return $ott;
    }
);

sub init {
    return 1;
}

sub run {
    my ( $self, $req, $action ) = @_;
    my $user = $req->userData->{ $self->conf->{whatToTrace} };
    return $self->p->sendError( $req, 'PE82', 400 )
      unless $user;

    # Check if TOTP can be updated
    my $msg = $self->canUpdateSfa( $req, $action );
    return $self->p->sendError( $req, $msg, 400 ) if $msg;

    # Verification that user has a valid TOTP app
    if ( $action eq 'verify' ) {

        # Get form token
        my $token = $req->param('token');
        unless ($token) {
            $self->userLogger->warn(
                "TOTP registration: register try without token for $user");
            return $self->p->sendError( $req, 'noTOTPFound', 400 );
        }

        # Verify that token exists in DB (note that "keep" flag is set to
        # permit more than 1 try during token life
        unless ( $token = $self->ott->getToken( $token, 1 ) ) {
            $self->userLogger->notice(
                "TOTP registration: token expired for $user");
            return $self->p->sendError( $req, 'PE82', 400 );
        }

        # Token is valid, so we have the master key proposed
        # ($token->{_totp2fSecret})

        # Now check TOTP code to verify that user has a valid TOTP app
        my $code     = $req->param('code');
        my $TOTPName = $req->param('TOTPName');
        my $epoch    = time();

     # Set default name if empty, check characters and truncate name if too long
        $TOTPName ||= $epoch;
        unless ( $TOTPName =~ /^[\w]+$/ ) {
            $self->userLogger->error('TOTP name with bad character(s)');
            return $self->p->sendError( $req, 'badName', 200 );
        }
        $TOTPName =
          substr( $TOTPName, 0, $self->conf->{max2FDevicesNameLength} );
        $self->logger->debug("TOTP name: $TOTPName");

        unless ($code) {
            $self->userLogger->info('TOTP registration: empty validation form');
            return $self->p->sendError( $req, 'missingCode', 200 );
        }

        my $r = $self->verifyCode(
            $self->conf->{totp2fInterval},
            $self->conf->{totp2fRange},
            $self->conf->{totp2fDigits},
            $token->{_totp2fSecret}, $code
        );
        if ( $r == -1 ) {
            return $self->p->sendError( $req, 'serverError', 500 );
        }

        # Invalid try is returned with a 200 code. Javascript will read error
        # and propose to retry
        elsif ( $r == 0 ) {
            $self->userLogger->notice(
                "TOTP registration: invalid TOTP for $user");
            return $self->p->sendError( $req, 'badCode', 200 );
        }
        $self->logger->debug('TOTP code verified');

        # Now code is verified, let's store the master key in persistent data
        my $secret = '';

        # Reading existing 2FDevices
        $self->logger->debug("Looking for 2F Devices...");
        my $_2fDevices;
        if ( $req->userData->{_2fDevices} ) {
            $_2fDevices = eval {
                from_json( $req->userData->{_2fDevices},
                    { allow_nonref => 1 } );
            };
            if ($@) {
                $self->logger->error("Corrupted session (_2fDevices): $@");
                return $self->p->sendError( $req, "serverError", 500 );
            }
        }
        else {
            $self->logger->debug("No 2F Device found");
            $_2fDevices = [];
        }

        # Reading existing TOTP
        my @totp2f = grep { $_->{type} eq 'TOTP' } @$_2fDevices;
        unless (@totp2f) {
            $self->logger->debug("No TOTP Device found");

            # Set default value
            push @totp2f, { _secret => '' };
        }

        # Loading TOTP secret
        $self->logger->debug("Reading TOTP secret if exists...");
        $secret = $_->{_secret} foreach (@totp2f);
        return $self->p->sendError( $req, 'totpExistingKey', 200 )
          if $secret;

        ### USER CAN ONLY REGISTER ONE TOTP ###
        # Delete TOTP previously registered
        $self->logger->debug("Looking for TOTP to delete...");
        my $size = my @keep =
          map { $_->{type} eq 'TOTP' ? () : $_ } @$_2fDevices;

        # Check if user can register one more device
        my $maxSize = $self->conf->{max2FDevices};
        $self->logger->debug("Registered 2F Device(s): $size / $maxSize");
        if ( $size >= $maxSize ) {
            $self->userLogger->warn("Max number of 2F devices is reached");
            return $self->p->sendError( $req, 'maxNumberof2FDevicesReached',
                400 );
        }

        my $storable_secret =
          $self->get_storable_secret( $token->{_totp2fSecret} );
        unless ($storable_secret) {
            $self->logger->error("Unable to encrypt TOTP secret");
            return $self->p->sendError( $req, "serverError", 500 );
        }

        # Store TOTP secret
        push @keep,
          {
            type    => 'TOTP',
            name    => $TOTPName,
            _secret => $storable_secret,
            epoch   => $epoch
          };
        $self->logger->debug(
            "Append 2F Device: { type => 'TOTP', name => $TOTPName }");
        $self->p->updatePersistentSession( $req,
            { _2fDevices => to_json( \@keep ) } );
        $self->userLogger->notice(
            "TOTP registration of $TOTPName succeeds for $user");
        return [
            200,
            [ 'Content-Type' => 'application/json', 'Content-Length' => 12, ],
            ['{"result":1}']
        ];
    }

    # Get or generate master key
    elsif ( $action eq 'getkey' ) {
        my $nk     = 0;
        my $secret = '';

        # Read existing 2FDevices
        $self->logger->debug("Looking for 2F Devices...");
        my $_2fDevices;
        if ( $req->userData->{_2fDevices} ) {
            $_2fDevices = eval {
                from_json( $req->userData->{_2fDevices},
                    { allow_nonref => 1 } );
            };
            if ($@) {
                $self->logger->error("Corrupted session (_2fDevices): $@");
                return $self->p->sendError( $req, "serverError", 500 );
            }
        }

        else {
            $self->logger->debug("No 2F Device found");
            $_2fDevices = [];
        }

        # Looking for TOTP
        my @totp2f = grep { $_->{type} eq "TOTP" } @$_2fDevices;
        unless (@totp2f) {
            $self->logger->debug("No TOTP found");

            # Set default value
            push @totp2f, { _secret => '' };
        }

        # Loading TOTP secret
        $self->logger->debug("Reading TOTP secret if exists...");
        $secret = $_->{_secret} foreach (@totp2f);

        if ($secret) {
            return $self->p->sendError( $req, 'totpExistingKey', 200 );
        }
        else {
            $secret = $self->newSecret;
            $self->logger->debug("Generating new secret = $secret");
            $nk = 1;
        }

        # Secret is stored in a token: we choose to not accept secret returned
        # by Ajax request to avoid some attacks
        my $token = $self->ott->createToken( {
                _totp2fSecret => $secret,
            }
        );

        my $issuer;
        unless ( $issuer = $self->conf->{totp2fIssuer} ) {
            $issuer = $self->conf->{portal};
            $issuer =~ s#^https?://([^/:]+).*$#$1#;
        }

        # QR-code will be generated by a javascript, here we just send data
        return $self->p->sendJSONresponse(
            $req,
            {
                secret   => $secret,
                token    => $token,
                portal   => $issuer,
                user     => $user,
                newkey   => $nk,
                digits   => $self->conf->{totp2fDigits},
                interval => $self->conf->{totp2fInterval}
            }
        );
    }

    # Delete TOTP
    elsif ( $action eq 'delete' ) {

        # Check if unregistration is allowed
        return $self->p->sendError( $req, 'notAuthorized', 400 )
          unless $self->conf->{totp2fUserCanRemoveKey};

        my $epoch = $req->param('epoch')
          or return $self->p->sendError( $req, '"epoch" parameter is missing',
            400 );

        # Read existing 2FDevices
        $self->logger->debug("Loading 2F Devices...");
        my ( $_2fDevices, $TOTPName );
        if ( $req->userData->{_2fDevices} ) {
            $_2fDevices = eval {
                from_json( $req->userData->{_2fDevices},
                    { allow_nonref => 1 } );
            };
            if ($@) {
                $self->logger->error("Corrupted session (_2fDevices): $@");
                return $self->p->sendError( $req, "serverError", 500 );
            }
        }
        else {
            $self->logger->debug("No 2F Device found");
            $_2fDevices = [];
        }

        # Delete TOTP 2F device
        @$_2fDevices = map {
            if ( $_->{epoch} eq $epoch ) { $TOTPName = $_->{name}; () }
            else                         { $_ }
        } @$_2fDevices;
        if ($TOTPName) {
            $self->logger->debug(
"Delete 2F Device: { type => 'TOTP', epoch => $epoch, name => $TOTPName }"
            );
            $self->p->updatePersistentSession( $req,
                { _2fDevices => to_json($_2fDevices) } );
            $self->userLogger->notice(
                "TOTP $TOTPName unregistration succeeds for $user");
            return [
                200,
                [
                    'Content-Type'   => 'application/json',
                    'Content-Length' => 12,
                ],
                ['{"result":1}']
            ];
        }
        else {
            $self->p->sendError( $req, '2FDeviceNotFound', 400 );
        }
    }
    else {
        $self->logger->error("Unknown TOTP action -> $action");
        return $self->p->sendError( $req, 'unknownAction', 400 );
    }
}

1;
