OneBot: Leantime 改造版源码(BOM/Univer 表格/AI 接管/品牌替换等)
This commit is contained in:
111
app/Domain/TwoFA/Controllers/Edit.php
Normal file
111
app/Domain/TwoFA/Controllers/Edit.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\TwoFA\Controllers;
|
||||
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Domain\TwoFA\Services\TwoFA as TwoFAService;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Edit extends Controller
|
||||
{
|
||||
private TwoFAService $twoFAService;
|
||||
|
||||
/**
|
||||
* Initializes dependencies.
|
||||
*/
|
||||
public function init(TwoFAService $twoFAService): void
|
||||
{
|
||||
$this->twoFAService = $twoFAService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the 2FA setup/edit page.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function get(array $params): Response
|
||||
{
|
||||
$this->assignSetupData();
|
||||
$this->generateFormTokens();
|
||||
|
||||
return $this->tpl->display('twofa.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles 2FA enable/disable actions.
|
||||
*
|
||||
* @param array $params Request parameters
|
||||
*
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
public function post(array $params): Response
|
||||
{
|
||||
$userId = (int) session('userdata.id');
|
||||
|
||||
if (isset($_POST['disable'])) {
|
||||
if ($this->isValidFormToken()) {
|
||||
$this->twoFAService->disable2FA($userId);
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.form_token_incorrect'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($_POST['save'])) {
|
||||
if (isset($_POST['secret'])) {
|
||||
$this->twoFAService->saveSecret($userId, $_POST['secret']);
|
||||
}
|
||||
|
||||
if (isset($_POST['secret'], $_POST['twoFACode'])) {
|
||||
if ($this->twoFAService->verifyAndEnable($userId, $_POST['secret'], $_POST['twoFACode'])) {
|
||||
$this->tpl->setNotification($this->language->__('notification.twoFA_enabled_success'), 'success', 'twoFAenabled');
|
||||
} else {
|
||||
$this->tpl->setNotification($this->language->__('notification.incorrect_twoFA_code'), 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->assignSetupData();
|
||||
$this->generateFormTokens();
|
||||
|
||||
return $this->tpl->display('twofa.edit');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns the current 2FA setup state (secret, QR data, enabled flag) to the template.
|
||||
*
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
private function assignSetupData(): void
|
||||
{
|
||||
$setup = $this->twoFAService->getSetupData((int) session('userdata.id'));
|
||||
|
||||
$this->tpl->assign('secret', $setup['secret']);
|
||||
$this->tpl->assign('twoFAEnabled', $setup['twoFAEnabled']);
|
||||
|
||||
if (! $setup['twoFAEnabled']) {
|
||||
$this->tpl->assign('qrData', $setup['qrData']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the submitted CSRF form token against the session.
|
||||
*/
|
||||
private function isValidFormToken(): bool
|
||||
{
|
||||
return isset($_POST[session('formTokenName')])
|
||||
&& $_POST[session('formTokenName')] == session('formTokenValue');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates CSRF form tokens for sensitive forms.
|
||||
*/
|
||||
private function generateFormTokens(): void
|
||||
{
|
||||
$permittedChars = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
session(['formTokenName' => substr(str_shuffle($permittedChars), 0, 32)]);
|
||||
session(['formTokenValue' => substr(str_shuffle($permittedChars), 0, 32)]);
|
||||
}
|
||||
}
|
||||
68
app/Domain/TwoFA/Controllers/Verify.php
Normal file
68
app/Domain/TwoFA/Controllers/Verify.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\TwoFA\Controllers;
|
||||
|
||||
use Leantime\Core\Controller\Controller;
|
||||
use Leantime\Core\Controller\Frontcontroller as FrontcontrollerCore;
|
||||
use Leantime\Domain\Auth\Services\Auth as AuthService;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class Verify extends Controller
|
||||
{
|
||||
private AuthService $authService;
|
||||
|
||||
/**
|
||||
* init - initialize private variables
|
||||
*
|
||||
* @params parameters or body of the request
|
||||
*/
|
||||
public function init(AuthService $authService)
|
||||
{
|
||||
$this->authService = $authService;
|
||||
}
|
||||
|
||||
/**
|
||||
* get - handle get requests
|
||||
*
|
||||
* @params parameters or body of the request
|
||||
*/
|
||||
public function get($params)
|
||||
{
|
||||
$redirectUrl = BASE_URL.'/dashboard/home';
|
||||
|
||||
if (isset($_GET['redirect'])) {
|
||||
$redirectUrl = BASE_URL.urldecode($_GET['redirect']);
|
||||
}
|
||||
|
||||
$this->tpl->assign('redirectUrl', $redirectUrl);
|
||||
|
||||
return $this->tpl->display('twofa.verify', 'entry');
|
||||
}
|
||||
|
||||
public function post($params): Response
|
||||
{
|
||||
|
||||
if (session()->exists('userdata') && $this->authService->use2FA()) {
|
||||
if (isset($params['twoFA_code']) === true) {
|
||||
$redirectUrl = filter_var($params['redirectUrl'], FILTER_SANITIZE_URL);
|
||||
|
||||
if ($this->authService->verify2FA($params['twoFA_code'])) {
|
||||
$this->authService->set2FAVerified();
|
||||
|
||||
return FrontcontrollerCore::redirect($redirectUrl);
|
||||
} else {
|
||||
$this->tpl->setNotification('notification.incorrect_twoFA_code', 'error');
|
||||
|
||||
return FrontcontrollerCore::redirect(BASE_URL.'/twoFA/verify');
|
||||
}
|
||||
} else {
|
||||
$this->tpl->setNotification('notification.incorrect_twoFA_code', 'error');
|
||||
|
||||
return FrontcontrollerCore::redirect(BASE_URL.'/twoFA/verify');
|
||||
}
|
||||
}
|
||||
|
||||
/** @todo make a 400 response page **/
|
||||
return $this->tpl->display('error.400');
|
||||
}
|
||||
}
|
||||
148
app/Domain/TwoFA/Services/TwoFA.php
Normal file
148
app/Domain/TwoFA/Services/TwoFA.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Leantime\Domain\TwoFA\Services;
|
||||
|
||||
use Endroid\QrCode\Color\Color;
|
||||
use Endroid\QrCode\Label\Label;
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Endroid\QrCode\Writer\PngWriter;
|
||||
use Leantime\Domain\Users\Repositories\Users as UserRepository;
|
||||
use RobThree\Auth\Providers\Qr\IQRCodeProvider;
|
||||
use RobThree\Auth\TwoFactorAuth;
|
||||
use RobThree\Auth\TwoFactorAuthException;
|
||||
|
||||
/**
|
||||
* Two-factor authentication domain service.
|
||||
*
|
||||
* Owns TOTP secret generation, QR-code construction, code verification, and
|
||||
* persistence of the user's 2FA state so controllers stay thin.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
class TwoFA
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepo
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Builds the data needed to render the 2FA setup page for a user.
|
||||
*
|
||||
* Generates a fresh TOTP secret (and matching QR code) when the user does
|
||||
* not already have one. The QR code is omitted when 2FA is already enabled.
|
||||
*
|
||||
* @param int $userId The user to build setup data for
|
||||
* @return array{secret: string, qrData: string|null, twoFAEnabled: bool}
|
||||
*
|
||||
* @throws TwoFactorAuthException
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function getSetupData(int $userId): array
|
||||
{
|
||||
$user = $this->userRepo->getUser($userId);
|
||||
$tfa = $this->createTwoFactorAuth();
|
||||
|
||||
$secret = $user['twoFASecret'] ?? '';
|
||||
if (empty($secret)) {
|
||||
$secret = $tfa->createSecret(160);
|
||||
}
|
||||
|
||||
$enabled = (bool) ($user['twoFAEnabled'] ?? false);
|
||||
|
||||
return [
|
||||
'secret' => $secret,
|
||||
'qrData' => $enabled ? null : $tfa->getQRCodeImageAsDataUri($user['username'], $secret),
|
||||
'twoFAEnabled' => $enabled,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a TOTP secret for the user without enabling 2FA.
|
||||
*
|
||||
* Stored ahead of verification so a failed code entry does not discard the
|
||||
* secret the user is mid-way through enrolling.
|
||||
*
|
||||
* @param int $userId The user to store the secret for
|
||||
* @param string $secret The TOTP secret
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function saveSecret(int $userId, string $secret): void
|
||||
{
|
||||
$this->userRepo->patchUser($userId, ['twoFASecret' => $secret]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies a submitted TOTP code against the secret and, when valid, enables 2FA.
|
||||
*
|
||||
* @param int $userId The user enabling 2FA
|
||||
* @param string $secret The TOTP secret
|
||||
* @param string $code The code submitted by the user
|
||||
* @return bool True when the code verified and 2FA was enabled
|
||||
*
|
||||
* @throws TwoFactorAuthException
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function verifyAndEnable(int $userId, string $secret, string $code): bool
|
||||
{
|
||||
$verified = $this->createTwoFactorAuth()->verifyCode($secret, $code);
|
||||
|
||||
if (! $verified) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->userRepo->patchUser($userId, [
|
||||
'twoFAEnabled' => 1,
|
||||
'twoFASecret' => $secret,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables 2FA for the user and clears their stored secret.
|
||||
*
|
||||
* @param int $userId The user to disable 2FA for
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public function disable2FA(int $userId): void
|
||||
{
|
||||
$this->userRepo->patchUser($userId, [
|
||||
'twoFAEnabled' => 0,
|
||||
'twoFASecret' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TwoFactorAuth instance backed by a PNG QR-code provider.
|
||||
*
|
||||
* @throws TwoFactorAuthException
|
||||
*/
|
||||
private function createTwoFactorAuth(): TwoFactorAuth
|
||||
{
|
||||
return new TwoFactorAuth('Leantime', 6, 30, 'sha1', new class implements IQRCodeProvider
|
||||
{
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
public function getQRCodeImage($qrtext, $size): string
|
||||
{
|
||||
$writer = new PngWriter;
|
||||
|
||||
$qrCode = new QrCode(data: $qrtext, size: $size, backgroundColor: new Color(255, 255, 255, 127));
|
||||
|
||||
$label = new Label(
|
||||
text: 'Label',
|
||||
textColor: new Color(255, 0, 0)
|
||||
);
|
||||
|
||||
return $writer->write($qrCode, null, null)->getString();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
59
app/Domain/TwoFA/Templates/edit.blade.php
Normal file
59
app/Domain/TwoFA/Templates/edit.blade.php
Normal file
@@ -0,0 +1,59 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pageicon"><span class="fa fa-lock"></span></div>
|
||||
<div class="pagetitle">
|
||||
<h1>{!! __('label.twoFA') !!}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="maincontent">
|
||||
<div class="maincontentinner">
|
||||
|
||||
{!! $tpl->displayNotification() !!}
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
|
||||
<h3>{!! __('label.twoFA_setup') !!}</h3>
|
||||
<br />
|
||||
<div class="center">
|
||||
|
||||
@if (! $twoFAEnabled)
|
||||
<h5>1. {!! __('text.twoFA_qr') !!}</h5>
|
||||
<br />
|
||||
<img src="{{ $qrData }}" style="border-radius: var(--box-radius);"/><br />
|
||||
Secret: <p>{{ $secret }}</p><br/>
|
||||
<form action="" method="post" class='stdform'>
|
||||
<h5>2. {!! __('text.twoFA_verify_code') !!}</h5>
|
||||
<p>
|
||||
<span>{!! __('label.twoFACode_short') !!}:</span>
|
||||
<x-global::forms.text-input name="twoFACode" id="twoFACode" /><br/>
|
||||
</p>
|
||||
|
||||
<input type="hidden" name="secret" value="{{ $secret }}" />
|
||||
<br/>
|
||||
<p class='stdformbutton'>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.save')" name="save" id="save" />
|
||||
</p>
|
||||
</form>
|
||||
@else
|
||||
<form action="" method="post" class='stdform'>
|
||||
<h5>{!! __('text.twoFA_already_enabled') !!}</h5>
|
||||
<input type="hidden" name="{{ session('formTokenName') }}" value="{{ session('formTokenValue') }}" />
|
||||
<p class='stdformbutton'>
|
||||
<x-global::forms.button tag="input" inputType="submit" contentRole="primary" :labelText="__('buttons.remove')" name="disable" id="disable" />
|
||||
<x-global::forms.button tag="a" link="{{ BASE_URL }}/users/editOwn">{!! __('buttons.back') !!}</x-global::forms.button>
|
||||
</p>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
31
app/Domain/TwoFA/Templates/verify.blade.php
Normal file
31
app/Domain/TwoFA/Templates/verify.blade.php
Normal file
@@ -0,0 +1,31 @@
|
||||
@extends($layout)
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="pageheader">
|
||||
<div class="pagetitle">
|
||||
<h1>{!! __('headlines.twoFA_login') !!}</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="regcontent">
|
||||
<form id="login" action="{{ BASE_URL }}/twoFA/verify" method="post">
|
||||
<input type="hidden" name="redirectUrl" value="{{ $redirectUrl }}"/>
|
||||
|
||||
{!! $tpl->displayInlineNotification() !!}
|
||||
|
||||
<div class="">
|
||||
<x-global::forms.text-input name="twoFA_code" id="twoFA_code"
|
||||
placeholder="{{ __('label.twoFACode') }}"
|
||||
value="" autofocus />
|
||||
</div>
|
||||
<div class="">
|
||||
<div class="forgotPwContainer">
|
||||
<a href="{{ BASE_URL }}/auth/logout" class="forgotPw">{!! __('menu.sign_out') !!}</a>
|
||||
</div>
|
||||
<x-global::forms.button tag="input" inputType="submit" name="login" :labelText="__('buttons.login')"
|
||||
contentRole="primary"/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
Reference in New Issue
Block a user