// auth.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

'use strict';

const express = require('express');
const mongoose = require('mongoose');
const passport = require('passport');
const uuidv4 = require('uuid').v4;

const { SiteController, SiteError } = require('../../lib/site-lib');

const ConnectToken = mongoose.model('ConnectToken');

class AuthController extends SiteController {

  constructor (dtp) {
    super(dtp, module.exports);
  }

  async start ( ) {
    const {
      coreNode: coreNodeService,
      limiter: limiterService,
    } = this.dtp.services;

    const upload = this.createMulter();

    const router = express.Router();
    this.dtp.app.use('/auth', router);

    const authRequired = this.dtp.services.session.authCheckMiddleware({ requireLogin: true });
    const authRequiredNoRedirect = this.dtp.services.session.authCheckMiddleware({ requireLogin: true, useRedirect: false });

    router.post(
      '/otp/enable',
      limiterService.createMiddleware(limiterService.config.auth.postOtpEnable),
      this.postOtpEnable.bind(this),
    );
    router.post(
      '/otp/auth',
      limiterService.createMiddleware(limiterService.config.auth.postOtpAuthenticate),
      this.postOtpAuthenticate.bind(this),
    );

    router.post(
      '/login',
      limiterService.createMiddleware(limiterService.config.auth.postLogin),
      upload.none(),
      this.postLogin.bind(this),
    );

    router.get(
      '/api-token/personal',
      authRequired,
      limiterService.createMiddleware(limiterService.config.auth.getPersonalApiToken),
      this.getPersonalApiToken.bind(this),
    );

    router.get(
      '/socket-token',
      authRequiredNoRedirect,
      limiterService.createMiddleware(limiterService.config.auth.getSocketToken),
      this.getSocketToken.bind(this),
    );

    await coreNodeService.attachExpressRoutes(router);

    router.get(
      '/core',
      limiterService.createMiddleware(limiterService.config.auth.getCoreHome),
      this.getCoreHome.bind(this),
    );

    router.get(
      '/logout',
      authRequired,
      limiterService.createMiddleware(limiterService.config.auth.getLogout),
      this.getLogout.bind(this),
    );

    return router;
  }

  async postOtpEnable (req, res, next) {
    const { otpAuth: otpAuthService } = this.dtp.services;
    
    const service = req.body['otp-service'];
    const secret = req.body['otp-secret'];
    const token = req.body['otp-token'];
    const otpRedirectURL = req.body['otp-redirect'] || '/';
    
    try {
      this.log.info('enabling OTP protections', { service, secret, token });
      res.locals.otpAccount = await otpAuthService.createOtpAccount(req, service, secret, token);
      res.locals.otpRedirectURL = otpRedirectURL;
      res.render('otp/new-account');
    } catch (error) {
      this.log.error('failed to enable OTP protections', {
        service, error,
      });
      return next(error);
    }
  }

  async postOtpAuthenticate (req, res, next) {
    const { otpAuth: otpAuthService } = this.dtp.services;

    if (!req.user) {
      return res.status(403).json({
        success: false,
        message: 'Must be logged in',
      });
    }
    const service = req.body['otp-service'];
    if (!service) {
      return res.status(400).json({
        success: false,
        message: 'Must specify OTP service name',
      });
    }
    const passcode = req.body['otp-passcode'];
    if (!passcode || (typeof passcode !== 'string') || (passcode.length !== 6)) {
      return res.status(400).json({
        success: false,
        message: 'Must include a valid passcode',
      });
    }
    try {
      await otpAuthService.startOtpSession(req, service, passcode);
      return res.redirect(req.body['otp-redirect']);
    } catch (error) {
      this.log.error('failed to verify one-time password for 2FA', {
        service, error,
      }, req.user);
      return next(error);
    }
  }

  async postLogin (req, res, next) {
    const redirectUri = req.session.loginReturnTo || '/';
    this.log.debug('starting passport.authenticate', { redirectUri });
    passport.authenticate('dtp-local', (error, user/*, info*/) => {
      if (error) {
        req.session.loginResult = error.toString();
        return next(error);
      }
      if (!user) {
        req.session.loginResult = 'Username or email address is unknown.';
        return res.redirect('/welcome/login');
      }
      this.log.info('user logging in', { user: user.username });
      req.login(user, async (error) => {
        if (error) {
          return next(error);
        }

        // scrub login return URL from session
        delete req.session.loginReturnTo;

        // redirect to whatever was wanted
        return res.redirect(redirectUri);
      });
    })(req, res, next);
  }

  async getPersonalApiToken (req, res, next) {
    try {
      const { apiGuard: apiGuardService } = this.dtp.platform.services;
      res.locals.apiToken = await apiGuardService.createApiToken(req.user, [
        'account-read',
        // additional scopes go here
      ]);
      res.render('api-token/view');
    } catch (error) {
      this.log.error('failed to generate API token', { error });
      return next(error);
    }
  }

  async getSocketToken (req, res, next) {
    try {
      const token = await ConnectToken.create({
        created: new Date(),
        userType: req.user.core ? 'CoreUser' : 'User',
        user: req.user._id,
        token: uuidv4(),
      });
      res.status(200).json({
        success: true,
        token: token.token
      });
    } catch (error) {
      this.log.error('failed to create Socket.io connect token', { error });
      return next(error);
    }
  }

  async getCoreHome (req, res, next) {
    const { coreNode: coreNodeService } = this.dtp.services;
    try {
      res.locals.currentView = 'welcome';
      res.locals.pagination = this.getPaginationParameters(req, 20);
      res.locals.connectedCores = await coreNodeService.getConnectedCores(res.locals.pagination);
      res.render('welcome/core-home');
    } catch (error) {
      this.log.error('failed to generate Core auth menu', { error });
      return next(error);
    }
  }

  async getLogout (req, res, next) {
    if (!req.user) {
      return next(new SiteError(403, 'You are not signed in'));
    }
    
    req.logout();
    req.session.destroy((err) => {
      if (err) {
        this.log.error('failed to destroy browser session', { err });
        return next(err);
      }
      res.redirect('/');
    });
  }
}

module.exports = {
  slug: 'auth',
  name: 'auth',
  create: async (dtp) => { return new AuthController(dtp); },
};
