addMinutes($ttlMinutes); MagicLink::query() ->where('user_id', $user->id) ->where('purpose', 'login') ->whereNull('consumed_at') ->update([ 'consumed_at' => now(), 'ip_consumed' => $requestedIp, ]); $magicLink = $user->magicLinks()->create([ 'token_hash' => $tokenHash, 'purpose' => 'login', 'expires_at' => $expiresAt, 'ip_requested' => $requestedIp, ]); return [ 'magic_link' => $magicLink, 'plain_token' => $plainToken, 'expires_at' => $expiresAt, ]; } /** * Public read-only share link for a press release. Token holder can view * the press release in any state (draft/review/rejected/published) until * the link expires. Default TTL: 7 days. * * @return array{magic_link: MagicLink, plain_token: string, url: string, expires_at: Carbon} */ public function createPressReleaseShareLink(PressRelease $pressRelease, ?User $issuer = null, int $ttlDays = 7): array { $plainToken = Str::random(64); $tokenHash = hash('sha256', $plainToken); $expiresAt = now()->addDays($ttlDays); $magicLink = MagicLink::query()->create([ 'user_id' => $issuer?->id ?? $pressRelease->user_id, 'token_hash' => $tokenHash, 'purpose' => 'press_release_access', 'payload' => [ 'press_release_id' => $pressRelease->id, ], 'expires_at' => $expiresAt, ]); return [ 'magic_link' => $magicLink, 'plain_token' => $plainToken, 'url' => route('press-releases.preview', ['token' => $plainToken]), 'expires_at' => $expiresAt, ]; } }