Laravel AT Protocol Client (alpha & unstable)
1<?php
2
3namespace SocialDept\AtpClient\Auth;
4
5use Illuminate\Contracts\Auth\Authenticatable;
6use SocialDept\AtpClient\Contracts\HasAtpSession;
7use SocialDept\AtpClient\Enums\Scope;
8use SocialDept\AtpClient\Enums\ScopeAuthorizationFailure;
9use SocialDept\AtpClient\Exceptions\ScopeAuthorizationException;
10use SocialDept\AtpClient\Session\Session;
11use SocialDept\AtpClient\Session\SessionManager;
12
13class ScopeGate
14{
15 protected ?Session $session = null;
16
17 public function __construct(
18 protected SessionManager $sessions,
19 protected ScopeChecker $checker,
20 ) {}
21
22 /**
23 * Set the session context directly.
24 */
25 public function forSession(Session $session): self
26 {
27 $instance = new self($this->sessions, $this->checker);
28 $instance->session = $session;
29
30 return $instance;
31 }
32
33 /**
34 * Set the session context via actor (handle or DID).
35 */
36 public function forUser(string $actor): self
37 {
38 $instance = new self($this->sessions, $this->checker);
39 $instance->session = $this->sessions->session($actor);
40
41 return $instance;
42 }
43
44 /**
45 * Check if the session has the given scope.
46 */
47 public function can(string|Scope $scope): bool
48 {
49 $session = $this->resolveSession();
50
51 if (! $session) {
52 return false;
53 }
54
55 return $this->checker->hasScope($session, $scope);
56 }
57
58 /**
59 * Check if the session has any of the given scopes.
60 *
61 * @param array<string|Scope> $scopes
62 */
63 public function canAny(array $scopes): bool
64 {
65 $session = $this->resolveSession();
66
67 if (! $session) {
68 return false;
69 }
70
71 foreach ($scopes as $scope) {
72 if ($this->checker->hasScope($session, $scope)) {
73 return true;
74 }
75 }
76
77 return false;
78 }
79
80 /**
81 * Check if the session has all of the given scopes.
82 *
83 * @param array<string|Scope> $scopes
84 */
85 public function canAll(array $scopes): bool
86 {
87 $session = $this->resolveSession();
88
89 if (! $session) {
90 return false;
91 }
92
93 return $this->checker->check($session, $scopes);
94 }
95
96 /**
97 * Check if the session does NOT have the given scope.
98 */
99 public function cannot(string|Scope $scope): bool
100 {
101 return ! $this->can($scope);
102 }
103
104 /**
105 * Authorize the session has all given scopes, or handle failure.
106 *
107 * @param string|Scope ...$scopes
108 *
109 * @throws ScopeAuthorizationException
110 */
111 public function authorize(string|Scope ...$scopes): void
112 {
113 if ($this->canAll($scopes)) {
114 return;
115 }
116
117 $session = $this->resolveSession();
118 $granted = $session ? $session->scopes() : [];
119 $required = array_map(
120 fn ($scope) => $scope instanceof Scope ? $scope->value : $scope,
121 $scopes
122 );
123 $missing = array_diff($required, $granted);
124
125 $exception = new ScopeAuthorizationException($missing, $granted);
126
127 $action = config('atp-client.scope_authorization.failure_action', ScopeAuthorizationFailure::Abort);
128
129 if ($action === ScopeAuthorizationFailure::Exception) {
130 throw $exception;
131 }
132
133 // For Abort and Redirect, let the exception render itself
134 throw $exception;
135 }
136
137 /**
138 * Get the granted scopes for the current session.
139 */
140 public function granted(): array
141 {
142 $session = $this->resolveSession();
143
144 return $session ? $session->scopes() : [];
145 }
146
147 /**
148 * Resolve the session from context.
149 */
150 protected function resolveSession(): ?Session
151 {
152 // If session was explicitly set, use it
153 if ($this->session) {
154 return $this->session;
155 }
156
157 // Try to resolve from authenticated user
158 $user = auth()->user();
159
160 if (! $user instanceof HasAtpSession) {
161 return null;
162 }
163
164 $did = $user->getAtpDid();
165
166 if (! $did) {
167 return null;
168 }
169
170 try {
171 return $this->sessions->session($did);
172 } catch (\Exception) {
173 return null;
174 }
175 }
176}