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

daycry / auth / 25518434194

07 May 2026 07:49PM UTC coverage: 58.608% (-6.4%) from 64.989%
25518434194

push

github

web-flow
Merge pull request #47 from daycry/development

Implement security enhancements and new account features

277 of 1030 new or added lines in 55 files covered. (26.89%)

11 existing lines in 6 files now uncovered.

3544 of 6047 relevant lines covered (58.61%)

47.97 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
use Daycry\Auth\Models\LoginModel;
20

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

156
        // Generate backup codes — shown to the user once on this page.
NEW
157
        $backupCodes = $user->generateBackupCodes();
×
158

159
        return $this->view(setting('Auth.views')['action_totp_setup_success'], [
×
160
            'redirectUrl' => url_to('security'),
×
NEW
161
            'backupCodes' => $backupCodes,
×
UNCOV
162
        ]);
×
163
    }
164

165
    /**
166
     * Disable TOTP for the current user.
167
     */
168
    public function totpDisable(): RedirectResponse
×
169
    {
170
        $code = (string) $this->request->getPost('token');
×
171
        $user = auth()->user();
×
172

173
        if (! $user->verifyTotpCode($code)) {
×
174
            return redirect()->back()->with('error', lang('Auth.invalidTotpToken'));
×
175
        }
176

177
        $user->disableTotp();
×
178

179
        return redirect()->route('security')->with('message', 'Two-factor authentication has been disabled.');
×
180
    }
181

182
    /**
183
     * User-facing login activity feed: lists the user's recent login attempts
184
     * (success + failure) so they can spot suspicious activity targeting
185
     * their account.
186
     *
187
     * Add the route, e.g.:
188
     *     $routes->get('account/security/activity',
189
     *         'Daycry\Auth\Controllers\UserSecurityController::loginActivity',
190
     *         ['as' => 'security-activity']);
191
     */
NEW
192
    public function loginActivity(): string
×
193
    {
NEW
194
        $user = auth()->user();
×
195

196
        /** @var LoginModel $loginModel */
NEW
197
        $loginModel = model(LoginModel::class);
×
198

NEW
199
        $limit = max(1, (int) ($this->request->getGet('limit') ?? 25));
×
NEW
200
        $limit = min($limit, 100);
×
201

NEW
202
        $entries = $loginModel->recentForUser($user, $limit);
×
203

NEW
204
        $views    = setting('Auth.views');
×
NEW
205
        $viewName = $views['security_login_activity'] ?? 'Daycry\Auth\Views\security\login_activity';
×
206

NEW
207
        return $this->view($viewName, [
×
NEW
208
            'entries' => $entries,
×
NEW
209
            'limit'   => $limit,
×
NEW
210
        ]);
×
211
    }
212
}
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