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

use strict;
use Lemonldap::NG::Portal::Main::Constants 'PE_OK';
use Mouse;
use JSON qw(from_json to_json);

our $VERSION = '2.23.0';

extends qw(
  Lemonldap::NG::Portal::2F::Register::Base
  Lemonldap::NG::Common::TOTP
);
with 'Lemonldap::NG::Portal::Lib::2fDevices';

# INITIALIZATION

has logo     => ( is => 'rw', default => 'totp.png' );
has prefix   => ( is => 'rw', default => 'totp' );
has template => ( is => 'ro', default => 'totp2fregister' );
has welcome  => ( is => 'ro', default => 'yourNewTotpKey' );
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;
    }
);

use constant supportedActions => {
    delete => "delete",
    modify => "modify",
    verify => "verify",
    getkey => "getkey",
};

sub verify {
    my ( $self, $req ) = @_;
    my $user = $req->userData->{ $self->conf->{whatToTrace} };

    return $self->failResponse( $req, 'csrfError', 400 )
      unless $self->checkCsrf($req);

    # Get form token
    my $token = $req->param('token');
    unless ($token) {
        return $self->failResponse( $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 ) ) {
        return $self->failResponse( $req, 'PE82', 400 );
    }

    # Now check TOTP code to verify that user has a valid TOTP app
    my $code = $req->param('code');
    unless ($code) {
        return $self->failResponse( $req, 'missingCode', 400 );
    }

    my $TOTPName =
      $self->checkNameSfa( $req, $self->type, $req->param('TOTPName') );
    return $self->failResponse( $req, 'badName', 200 ) unless $TOTPName;

    my $verify_result = $self->verify_totp(
        interval        => $self->conf->{totp2fInterval},
        digits          => $self->conf->{totp2fDigits},
        hash            => $self->conf->{totp2fAlgorithm},
        range_tolerance => $self->conf->{totp2fRange},
        stored_secret   => $token->{_totp2fSecret},
        code            => $code,
    );

    my $r = $verify_result->{result};

    if ( $r == -1 ) {
        $self->logger->error(
            "TOTP registration failed: " . $verify_result->{error} );
        return $self->failResponse( $req, 'serverError' );
    }

    # Invalid try is returned with a 200 code. Javascript will read error
    # and propose to retry
    if ( $r == 0 ) {
        return $self->failResponse( $req, 'badCode', 200 );
    }

    my $range = $verify_result->{offset};

    $self->userLogger->info("Codes match at range $range");
    $self->logger->debug( $self->prefix . '2f: code verified' );

    my $storable_secret = $self->get_storable_secret( $token->{_totp2fSecret} );
    unless ($storable_secret) {
        $self->logger->error( $self->prefix . '2f: unable to encrypt secret' );
        return $self->failResponse( $req, "serverError" );
    }

    # Store TOTP secret
    my $res = $self->registerDevice(
        $req,
        $req->userData,
        {
            _secret => $storable_secret,
            type    => $self->type,
            name    => $TOTPName,
            epoch   => time()
        }
    );
    if ( $res == PE_OK ) {
        return $self->successResponse( $req, { result => 1 } );
    }
    else {
        $self->logger->error( $self->prefix . "2f: unable to add device" );
        return $self->failResponse( $req, "PE$res" );
    }
}

sub getkey {
    my ( $self, $req ) = @_;
    my $user = $req->userData->{ $self->conf->{whatToTrace} };
    my ( $nk, $secret, $issuer ) = ( 0, '' );

    return $self->failResponse( $req, 'csrfError', 400 )
      unless $self->checkCsrf($req);

    $secret = $self->newSecret;
    $self->logger->debug( $self->prefix . "2f: generate 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,
        }
    );
    unless ( $issuer = $self->conf->{totp2fIssuer} ) {
        $issuer = $req->portal;
        $issuer =~ s#^https?://([^/:]+).*$#$1#;
    }

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

1;
