client->send('POST', '/api/auth/sessions', [ 'email' => $email, 'password' => $password, ]); $body = $response->json() ?? []; $accessToken = $body['accessToken'] ?? $body['access_token'] ?? null; if (! is_string($accessToken) || $accessToken === '') { throw new InsForgeException(null, 500, 'InsForge login response did not contain an access token.'); } Session::put(self::SESSION_ACCESS_TOKEN, $accessToken); Session::put(self::SESSION_REFRESH_COOKIE, $this->extractRefreshCookie($response) ?? ($body['refreshToken'] ?? null)); $authUser = is_array($body['user'] ?? null) ? $body['user'] : []; $profile = $this->ensureProfile( id: $authUser['id'] ?? null, email: $email, password: $password, fallbackName: $authUser['name'] ?? (is_array($authUser['profile'] ?? null) ? ($authUser['profile']['name'] ?? null) : null) ?? explode('@', $email)[0], ); return $this->hydrateProfile($profile); } public function profileById(?string $id): ?User { if ($id === null) { return null; } $profile = $this->client ->withToken(Session::get(self::SESSION_ACCESS_TOKEN)) ->getRecord('users', $id); if ($profile === null || ! ($profile['is_active'] ?? false)) { return null; } return $this->hydrateProfile($profile); } public function refreshAccessToken(): bool { $cookie = Session::get(self::SESSION_REFRESH_COOKIE); if (! is_string($cookie) || $cookie === '') { return false; } try { $response = $this->client->send('POST', '/api/auth/refresh', headers: ['Cookie' => $cookie]); } catch (InsForgeException) { return false; } $accessToken = ($response->json() ?? [])['accessToken'] ?? ($response->json() ?? [])['access_token'] ?? null; if (! is_string($accessToken) || $accessToken === '') { return false; } Session::put(self::SESSION_ACCESS_TOKEN, $accessToken); return true; } public function clearSession(): void { Session::forget([self::SESSION_ACCESS_TOKEN, self::SESSION_REFRESH_COOKIE]); } public static function accessTokenExpiresInSeconds(): ?int { $token = Session::get(self::SESSION_ACCESS_TOKEN); if (! is_string($token) || substr_count($token, '.') !== 2) { return null; } $payload = json_decode(base64_decode(strtr(explode('.', $token)[1], '-_', '+/').str_repeat('=', 4 - strlen(explode('.', $token)[1]) % 4), true), true); return isset($payload['exp']) ? (int) $payload['exp'] - time() : null; } private function ensureProfile(?string $id, string $email, string $password, string $fallbackName): array { $existing = $this->client->listRecords('users', ['email' => 'eq.'.$email])[0] ?? null; if ($existing !== null) { return $existing; } $attributes = array_filter([ 'id' => $id, 'name' => $fallbackName, 'email' => $email, 'password' => Hash::make($password), 'role' => 'operator', 'is_active' => true, ], fn ($value) => $value !== null); $this->client->insertRecord('users', [$attributes], asUser: false); return $this->client->listRecords('users', ['email' => 'eq.'.$email])[0] ?? $attributes; } private function hydrateProfile(array $profile): User { $user = new User; $user->forceFill([ 'id' => $profile['id'], 'name' => $profile['name'] ?? '', 'email' => $profile['email'], 'password' => $profile['password'] ?? '', 'role' => $profile['role'] ?? 'operator', 'is_active' => (bool) ($profile['is_active'] ?? false), ]); return $user; } private function extractRefreshCookie($response): ?string { foreach ((array) $response->headers() as $name => $values) { if (strtolower((string) $name) !== 'set-cookie') { continue; } foreach ((array) $values as $value) { $firstPair = explode(';', trim((string) $value))[0]; if (str_contains($firstPair, 'refresh')) { return $firstPair; } } } return null; } }