Feature: Customer Account Services
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Auth\Services;
|
||||
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Modules\Core\Auth\Models\UserSession;
|
||||
|
||||
/**
|
||||
* The record/revoke half of the session registry — see
|
||||
* database/migrations/2026_09_15_000001_create_user_sessions_table.php's
|
||||
* own docblock for why this exists (SESSION_DRIVER=redis in this app has
|
||||
* no "sessions" table to purge by user_id). The enforcement half is
|
||||
* Modules\Core\Auth\Http\Middleware\EnsureSessionNotRevoked, which reads
|
||||
* the token this class stamps into the session payload.
|
||||
*/
|
||||
class UserSessionService
|
||||
{
|
||||
private const SESSION_TOKEN_KEY = 'user_session_token';
|
||||
|
||||
/**
|
||||
* Called once, right after Auth::login() succeeds (see
|
||||
* UserOtpService::validate()) — generates a fresh token, records it,
|
||||
* and stamps it into the CURRENT session payload so
|
||||
* EnsureSessionNotRevoked can look it up on later requests.
|
||||
*/
|
||||
public function record(Authenticatable $user, ?Request $request = null): UserSession
|
||||
{
|
||||
$token = Str::random(64);
|
||||
|
||||
$session = UserSession::create([
|
||||
'user_id' => $user->getAuthIdentifier(),
|
||||
'token' => $token,
|
||||
'user_agent' => $request?->userAgent(),
|
||||
'ip_address' => $request?->ip(),
|
||||
'last_used_at' => now(),
|
||||
]);
|
||||
|
||||
session([self::SESSION_TOKEN_KEY => $token]);
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes every OTHER active session for $user — the current one
|
||||
* (matched by the token in the CURRENT session payload) is left
|
||||
* alone, matching Laravel's own logoutOtherDevices() semantics
|
||||
* (there just isn't a password to re-verify against here — this is a
|
||||
* passwordless account, so revocation is simply "every row that
|
||||
* isn't the one making this request").
|
||||
*
|
||||
* Known, deliberately accepted gap: this requires only a currently
|
||||
* valid session, not a freshly-completed login — so anyone holding
|
||||
* an already-authenticated session (e.g. someone who sits down at an
|
||||
* account left logged in on a shared/public PC) can use this to
|
||||
* evict the real owner's OTHER sessions just as easily as the real
|
||||
* owner could use it to evict an intruder's. A stricter version would
|
||||
* require a fresh OTP re-verification (e.g. within the last few
|
||||
* minutes) before allowing this call. Left as-is for now — revisit if
|
||||
* this turns out to matter in practice, rather than building
|
||||
* abuse-resistance against a threat model nobody's confirmed is real
|
||||
* for this storefront.
|
||||
*/
|
||||
public function revokeOtherSessions(Authenticatable $user): int
|
||||
{
|
||||
$currentToken = session(self::SESSION_TOKEN_KEY);
|
||||
|
||||
return UserSession::query()
|
||||
->where('user_id', $user->getAuthIdentifier())
|
||||
->whereNull('revoked_at')
|
||||
->when($currentToken, fn ($query) => $query->where('token', '!=', $currentToken))
|
||||
->update(['revoked_at' => now()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Revokes EVERY session for $user, current one included — for a
|
||||
* "this account may be compromised" response, not a routine logout.
|
||||
*/
|
||||
public function revokeAllSessions(Authenticatable $user): int
|
||||
{
|
||||
return UserSession::query()
|
||||
->where('user_id', $user->getAuthIdentifier())
|
||||
->whereNull('revoked_at')
|
||||
->update(['revoked_at' => now()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserSession|null null if the CURRENT session has no
|
||||
* recorded token at all (e.g. a session predating this feature, or
|
||||
* one Auth::login() established outside UserOtpService) — treated
|
||||
* as valid by EnsureSessionNotRevoked rather than rejected, since
|
||||
* there's nothing to have been revoked.
|
||||
*/
|
||||
public function currentSession(): ?UserSession
|
||||
{
|
||||
$token = session(self::SESSION_TOKEN_KEY);
|
||||
|
||||
if (! $token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return UserSession::where('token', $token)->first();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user