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

daycry / auth / 25545552027

08 May 2026 08:28AM UTC coverage: 59.045% (+0.4%) from 58.608%
25545552027

push

github

daycry
Add Laravel-parity auth features: Gates, Password Confirmation, Basic auth

Three independent additions that close the gaps identified when
comparing daycry/auth against Laravel Fortify + Sanctum + core. All
three are aditive — no existing API changes, no migrations needed.

F1. Gates & Policies (src/Authorization/)
- New `Gate` service (registered as `service('gate')`) with closure-
  and class-based authorization rules.
  - `define('post.update', fn($user, $post) => …)` for inline rules.
  - `policy(Post::class, PostPolicy::class)` for class-based policies.
  - `allows() / denies() / authorize() / forUser() / has()`.
  - Auto-discovery: `App\Models\Post` → `App\Policies\PostPolicy`,
    namespace configurable via `Auth::$policyNamespace` (default
    `App\Policies\`); toggle via `Auth::$gateAutoDiscover`.
  - Ability names like `post.update` resolve to method `update()` on
    the policy; bare names work too.
- Abstract `Policy` base with optional `before()` hook for global
  short-circuits (e.g. admins bypass everything on a resource).
- `PolicyResponse::allow($msg)` / `::deny($msg)` to carry deny-reason
  text; `->authorize()` throws `AuthorizationException` on deny.
- Brand-new `Daycry\Auth\Authorization\AuthorizationException`
  (distinct from the pre-existing `Exceptions\AuthorizationException`
  used by RBAC errors); carries the PolicyResponse.
- New `gate:ability,…` filter alias for ability-only route guards.
  Use the Gate API directly inside controllers when the check needs a
  resource argument.
- `Authorizable` trait gains `canDo()` / `cantDo()` — sister methods
  to the existing `can()` / `cant()`. The originals still take
  permission strings only ("posts.update"), the new ones route
  through the Gate and accept resource arguments. Signature of
  `can()` is unchanged.

F2. Password Confirmation workflow ("sudo mode")
- New `password-confirm` filter alias backed by `PasswordConfirmFilter`.
  Reads `password_confirmed_at` from the sessio... (continued)

128 of 182 new or added lines in 10 files covered. (70.33%)

3672 of 6219 relevant lines covered (59.04%)

48.17 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 CodeIgniter\HTTP\ResponseInterface;
18
use Daycry\Auth\Authentication\Passwords;
19
use Daycry\Auth\Libraries\TOTP;
20
use Daycry\Auth\Models\DeviceSessionModel;
21
use Daycry\Auth\Models\LoginModel;
22
use Daycry\Auth\Services\AuditLogger;
23

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

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

60
        /** @var DeviceSessionModel $deviceModel */
61
        $deviceModel = model(DeviceSessionModel::class);
×
62

63
        $sessions   = $deviceModel->getActiveForUser($user);
×
64
        $currentSid = session_id();
×
65

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

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

80
        if ($sessionId === '') {
×
81
            return redirect()->back()->with('error', 'Invalid session.');
×
82
        }
83

84
        $user = auth()->user();
×
85

86
        /** @var DeviceSessionModel $deviceModel */
87
        $deviceModel = model(DeviceSessionModel::class);
×
88

89
        // Safety check: ensure the session belongs to the current user
90
        $session = $deviceModel->findBySessionId($sessionId);
×
91

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

96
        $deviceModel->terminateSession($sessionId);
×
97

98
        return redirect()->back()->with('message', 'Session revoked successfully.');
×
99
    }
100

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

108
        /** @var DeviceSessionModel $deviceModel */
109
        $deviceModel = model(DeviceSessionModel::class);
×
110
        $deviceModel->terminateAllForUser($user, session_id());
×
111

112
        return redirect()->back()->with('message', 'All other sessions have been revoked.');
×
113
    }
114

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

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

129
        $otpAuthUrl = $user->enableTotp();
×
130
        $secret     = $user->getTotpSecret();
×
131

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

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

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

156
        // Mark the secret as confirmed — from this point hasTotpEnabled() returns true.
157
        $user->confirmTotp();
×
158

159
        // Generate backup codes — shown to the user once on this page.
160
        $backupCodes = $user->generateBackupCodes();
×
161

162
        return $this->view(setting('Auth.views')['action_totp_setup_success'], [
×
163
            'redirectUrl' => url_to('security'),
×
164
            'backupCodes' => $backupCodes,
×
165
        ]);
×
166
    }
167

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

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

180
        $user->disableTotp();
×
181

182
        return redirect()->route('security')->with('message', 'Two-factor authentication has been disabled.');
×
183
    }
184

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

199
        /** @var LoginModel $loginModel */
200
        $loginModel = model(LoginModel::class);
×
201

202
        $limit = max(1, (int) ($this->request->getGet('limit') ?? 25));
×
203
        $limit = min($limit, 100);
×
204

205
        $entries = $loginModel->recentForUser($user, $limit);
×
206

207
        $views    = setting('Auth.views');
×
208
        $viewName = $views['security_login_activity'] ?? 'Daycry\Auth\Views\security\login_activity';
×
209

210
        return $this->view($viewName, [
×
211
            'entries' => $entries,
×
212
            'limit'   => $limit,
×
213
        ]);
×
214
    }
215

216
    /**
217
     * Renders the password confirmation form.
218
     *
219
     * Wire as:
220
     *     $routes->get('auth/confirm-password',
221
     *         'Daycry\Auth\Controllers\UserSecurityController::confirmPasswordView',
222
     *         ['filter' => 'session', 'as' => 'password-confirm-show']);
223
     */
NEW
224
    public function confirmPasswordView(): ResponseInterface
×
225
    {
NEW
226
        $views    = setting('Auth.views');
×
NEW
227
        $viewName = $views['confirm_password'] ?? 'Daycry\Auth\Views\confirm_password';
×
228

NEW
229
        $content = $this->view($viewName);
×
230

NEW
231
        return $this->response->setBody($content);
×
232
    }
233

234
    /**
235
     * Verifies the submitted password against the logged-in user, stamps
236
     * `password_confirmed_at` in the session, then redirects to the URL
237
     * the user was originally trying to reach (or to a fallback).
238
     *
239
     * Wire as:
240
     *     $routes->post('auth/confirm-password',
241
     *         'Daycry\Auth\Controllers\UserSecurityController::confirmPasswordAction',
242
     *         ['filter' => 'session', 'as' => 'password-confirm']);
243
     */
NEW
244
    public function confirmPasswordAction(): RedirectResponse
×
245
    {
NEW
246
        $user     = auth()->user();
×
NEW
247
        $password = (string) $this->request->getPost('password');
×
248

NEW
249
        if ($user === null || $password === '') {
×
NEW
250
            return redirect()->route('password-confirm-show')
×
NEW
251
                ->with('error', lang('Auth.invalidPassword'));
×
252
        }
253

254
        /** @var Passwords $passwords */
NEW
255
        $passwords = service('passwords');
×
256

NEW
257
        if (! $passwords->verify($password, $user->getPasswordHash())) {
×
NEW
258
            return redirect()->route('password-confirm-show')
×
NEW
259
                ->with('error', lang('Auth.invalidPassword'));
×
260
        }
261

NEW
262
        session()->set('password_confirmed_at', time());
×
263

NEW
264
        (new AuditLogger())->record(AuditLogger::EVENT_PASSWORD_CONFIRMED, (int) $user->id);
×
265

NEW
266
        $intended = (string) (session()->getTempdata('passwordConfirmIntendedUrl') ?? '');
×
NEW
267
        session()->removeTempdata('passwordConfirmIntendedUrl');
×
268

NEW
269
        if ($intended === '') {
×
NEW
270
            $intended = config('Auth')->loginRedirect();
×
271
        }
272

NEW
273
        return redirect()->to($intended);
×
274
    }
275
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc