• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 22551289947

01 Mar 2026 07:53PM UTC coverage: 63.76% (+0.09%) from 63.668%
22551289947

push

github

daycry
Add pending TOTP flow and local QR generation

Introduce a two-phase TOTP enrollment (PENDING → CONFIRMED) via a new TotpState enum and confirmTotp() method. Store TOTP secrets encrypted (AES) and transparently decrypt in getTotpSecret(); enableTotp() now creates a pending secret. Replace Google Charts QR URLs with locally generated PNG data URIs using endroid/qr-code (added dependency). Update UserSecurityController comments/flow to keep pending secrets on verification failure and mark confirmed on success. Adjust tests and inject a fixed Encryption key for tests; update phpstan baseline counts accordingly.

19 of 25 new or added lines in 3 files covered. (76.0%)

1 existing line in 1 file now uncovered.

3100 of 4862 relevant lines covered (63.76%)

44.01 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Controllers/UserSecurityController.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Controllers;
15

16
use CodeIgniter\HTTP\RedirectResponse;
17
use Daycry\Auth\Libraries\TOTP;
18
use Daycry\Auth\Models\DeviceSessionModel;
19

20
/**
21
 * UserSecurityController
22
 *
23
 * Lets authenticated users manage their own security settings:
24
 *   - Active device sessions (list + revoke)
25
 *   - TOTP 2FA setup / disable
26
 *
27
 * Routes to add in your app (example):
28
 *   $routes->group('account/security', ['filter' => 'session'], static function ($routes): void {
29
 *       $routes->get('/',                  'Daycry\Auth\Controllers\UserSecurityController::index',             ['as' => 'security']);
30
 *       $routes->post('sessions/revoke',   'Daycry\Auth\Controllers\UserSecurityController::revokeSession',     ['as' => 'security-revoke-session']);
31
 *       $routes->post('sessions/revoke-all','Daycry\Auth\Controllers\UserSecurityController::revokeAllSessions',['as' => 'security-revoke-all']);
32
 *       $routes->get('totp/setup',         'Daycry\Auth\Controllers\UserSecurityController::totpSetup',        ['as' => 'totp-setup']);
33
 *       $routes->post('totp/setup/confirm','Daycry\Auth\Controllers\UserSecurityController::totpSetupConfirm', ['as' => 'totp-setup-confirm']);
34
 *       $routes->post('totp/disable',      'Daycry\Auth\Controllers\UserSecurityController::totpDisable',      ['as' => 'totp-disable']);
35
 *       $routes->post('sessions/password', 'Daycry\Auth\Controllers\UserSecurityController::changePassword',   ['as' => 'security-change-password']);
36
 *       $routes->get('email/change',       'Daycry\Auth\Controllers\UserSecurityController::changeEmailView',  ['as' => 'security-change-email']);
37
 *       $routes->post('email/change',      'Daycry\Auth\Controllers\UserSecurityController::changeEmail',      ['as' => 'security-change-email-action']);
38
 *       $routes->get('email/confirm',      'Daycry\Auth\Controllers\UserSecurityController::confirmEmailChange',['as' => 'security-confirm-email']);
39
 *       $routes->post('oauth/unlink',      'Daycry\Auth\Controllers\UserSecurityController::unlinkOauth',      ['as' => 'security-unlink-oauth']);
40
 *   });
41
 */
42
class UserSecurityController extends BaseAuthController
43
{
44
    protected function getValidationRules(): array
×
45
    {
46
        return [];
×
47
    }
48

49
    /**
50
     * Security overview: device sessions + TOTP status.
51
     */
52
    public function index(): string
×
53
    {
54
        $user = auth()->user();
×
55

56
        /** @var DeviceSessionModel $deviceModel */
57
        $deviceModel = model(DeviceSessionModel::class);
×
58

59
        $sessions   = $deviceModel->getActiveForUser($user);
×
60
        $currentSid = session_id();
×
61

62
        return $this->view(setting('Auth.views')['security_overview'], [
×
63
            'sessions'    => $sessions,
×
64
            'currentSid'  => $currentSid,
×
65
            'totpEnabled' => $user->hasTotpEnabled(),
×
66
        ]);
×
67
    }
68

69
    /**
70
     * Revoke a single device session.
71
     */
72
    public function revokeSession(): RedirectResponse
×
73
    {
74
        $sessionId = (string) $this->request->getPost('session_id');
×
75

76
        if ($sessionId === '') {
×
77
            return redirect()->back()->with('error', 'Invalid session.');
×
78
        }
79

80
        $user = auth()->user();
×
81

82
        /** @var DeviceSessionModel $deviceModel */
83
        $deviceModel = model(DeviceSessionModel::class);
×
84

85
        // Safety check: ensure the session belongs to the current user
86
        $session = $deviceModel->findBySessionId($sessionId);
×
87

88
        if ($session === null || $session->user_id !== $user->id) {
×
89
            return redirect()->back()->with('error', 'Session not found.');
×
90
        }
91

92
        $deviceModel->terminateSession($sessionId);
×
93

94
        return redirect()->back()->with('message', 'Session revoked successfully.');
×
95
    }
96

97
    /**
98
     * Revoke all other active sessions (keep current one).
99
     */
100
    public function revokeAllSessions(): RedirectResponse
×
101
    {
102
        $user = auth()->user();
×
103

104
        /** @var DeviceSessionModel $deviceModel */
105
        $deviceModel = model(DeviceSessionModel::class);
×
106
        $deviceModel->terminateAllForUser($user, session_id());
×
107

108
        return redirect()->back()->with('message', 'All other sessions have been revoked.');
×
109
    }
110

111
    /**
112
     * Show the TOTP setup page with QR code.
113
     *
114
     * Always generates a fresh secret. If the user navigates away before
115
     * confirming, a new QR code will be issued on their next visit.
116
     */
117
    public function totpSetup(): RedirectResponse|string
×
118
    {
119
        $user = auth()->user();
×
120

121
        if ($user->hasTotpEnabled()) {
×
122
            return redirect()->route('security')->with('error', 'TOTP is already enabled.');
×
123
        }
124

125
        $otpAuthUrl = $user->enableTotp();
×
126
        $secret     = $user->getTotpSecret();
×
127

128
        return $this->view(setting('Auth.views')['action_totp_setup_show'], [
×
129
            'otpAuthUrl' => $otpAuthUrl,
×
130
            'secret'     => $secret ?? '',
×
131
            'qrCodeUrl'  => TOTP::getQRCodeUrl($otpAuthUrl),
×
132
            'confirmUrl' => url_to('totp-setup-confirm'),
×
133
        ]);
×
134
    }
135

136
    /**
137
     * Confirm TOTP setup by verifying the first code.
138
     *
139
     * On failure the pending secret is kept so the user can retry with the same
140
     * QR code already in their authenticator app.
141
     */
142
    public function totpSetupConfirm(): RedirectResponse|string
×
143
    {
144
        $user = auth()->user();
×
145
        $code = (string) $this->request->getPost('token');
×
146

147
        if (! $user->verifyTotpCode($code)) {
×
UNCOV
148
            return redirect()->route('totp-setup')
×
149
                ->with('error', lang('Auth.totpSetupInvalidCode'));
×
150
        }
151

152
        // Mark the secret as confirmed — from this point hasTotpEnabled() returns true.
NEW
153
        $user->confirmTotp();
×
154

155
        return $this->view(setting('Auth.views')['action_totp_setup_success'], [
×
156
            'redirectUrl' => url_to('security'),
×
157
        ]);
×
158
    }
159

160
    /**
161
     * Disable TOTP for the current user.
162
     */
163
    public function totpDisable(): RedirectResponse
×
164
    {
165
        $code = (string) $this->request->getPost('token');
×
166
        $user = auth()->user();
×
167

168
        if (! $user->verifyTotpCode($code)) {
×
169
            return redirect()->back()->with('error', lang('Auth.invalidTotpToken'));
×
170
        }
171

172
        $user->disableTotp();
×
173

174
        return redirect()->route('security')->with('message', 'Two-factor authentication has been disabled.');
×
175
    }
176
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc