@ -1,5 +1,6 @@
<!DOCTYPE html>
<!DOCTYPE html>
< html lang = "en" >
< html lang = "en" >
< head >
< head >
< meta charset = "utf-8" >
< meta charset = "utf-8" >
< meta http-equiv = "X-UA-Compatible" content = "IE=edge" >
< meta http-equiv = "X-UA-Compatible" content = "IE=edge" >
@ -7,93 +8,340 @@
< title > {{ config_cache('app.name') }} - Authorization< / title >
< title > {{ config_cache('app.name') }} - Authorization< / title >
< link href = "{{ mix('/css/app.css') }}" rel = "stylesheet" >
< link href = "{{ mix('/css/app.css') }}" rel = "stylesheet" >
< style >
< style >
.passport-authorize .container {
.passport-authorize {
margin-top: 30px;
background-color: #f5f6f8;
min-height: 100vh;
}
.passport-authorize .authorize-wrap {
max-width: 460px;
margin: 0 auto;
padding: 40px 16px 32px;
}
.passport-authorize .authorize-card {
border-radius: 14px;
}
.passport-authorize .authorize-card .card-body {
padding: 2rem;
}
/* App -> account handoff */
.passport-authorize .handoff {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.25rem;
}
.passport-authorize .handoff-tile {
width: 64px;
height: 64px;
flex: 0 0 64px;
}
.passport-authorize .handoff-app {
display: flex;
align-items: center;
justify-content: center;
border-radius: 16px;
background-color: #343a40;
color: #fff;
font-size: 1.5rem;
font-weight: 700;
}
.passport-authorize .handoff-avatar {
border-radius: 50%;
object-fit: cover;
background-color: #fff;
border: 1px solid #e9ecef;
}
.passport-authorize .handoff-link {
position: relative;
width: 64px;
display: flex;
align-items: center;
justify-content: center;
color: #adb5bd;
}
.passport-authorize .handoff-link::before {
content: '';
position: absolute;
left: 6px;
right: 6px;
top: 50%;
border-top: 2px dashed #dee2e6;
}
}
.passport-authorize .scopes {
.passport-authorize .handoff-link i {
margin-top: 20px;
position: relative;
padding: 0 6px;
background-color: #fff;
font-size: 0.875rem;
}
}
.passport-authorize .buttons {
/* Scope list */
margin-top: 25px;
.passport-authorize .scope-list {
border: 1px solid #e9ecef;
border-radius: 10px;
overflow: hidden;
}
.passport-authorize .scope-item {
display: flex;
align-items: center;
padding: 12px 14px;
border-top: 1px solid #e9ecef;
}
.passport-authorize .scope-list>.scope-item:first-child {
border-top: 0;
}
.passport-authorize .scope-icon {
flex: 0 0 38px;
width: 38px;
height: 38px;
margin-right: 12px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
background-color: #eef1f4;
color: #495057;
}
.passport-authorize .scope-icon-warning {
background-color: #fff3cd;
color: #856404;
}
.passport-authorize .scope-icon-danger {
background-color: #f8d7da;
color: #842029;
}
.passport-authorize .scope-body {
flex: 1 1 auto;
min-width: 0;
}
.passport-authorize .scope-desc {
margin: 0;
font-size: 0.9375rem;
line-height: 1.3;
}
.passport-authorize .scope-id {
display: block;
margin-top: 2px;
font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.75rem;
color: #868e96;
}
/* Expander for extra scopes (no JS needed) */
.passport-authorize .scope-more>summary {
list-style: none;
cursor: pointer;
user-select: none;
padding: 10px 14px;
border-top: 1px solid #e9ecef;
text-align: center;
text-align: center;
font-size: 0.875rem;
font-weight: 600;
color: #007bff;
}
}
.passport-authorize .btn {
.passport-authorize .scope-more>summary::-webkit-details-marker {
width: 125px;
display: none ;
}
}
.passport-authorize .btn-approve {
.passport-authorize .scope-more>summary:hover {
margin-right: 15px;
background-color: #f8f9fa ;
}
}
.passport-authorize .scope-more>summary:focus-visible {
outline: 2px solid #007bff;
outline-offset: -2px;
}
.passport-authorize .scope-more>summary .fa-chevron-down {
margin-left: 6px;
font-size: 0.75rem;
transition: transform 0.15s ease;
}
.passport-authorize .scope-more[open]>summary .fa-chevron-down {
transform: rotate(180deg);
}
.passport-authorize .scope-more:not([open]) .when-open,
.passport-authorize .scope-more[open] .when-closed {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.passport-authorize .scope-more>summary .fa-chevron-down {
transition: none;
}
}
/* Actions */
.passport-authorize form {
.passport-authorize form {
display: inline;
margin: 0;
}
.passport-authorize .btn-block {
padding-top: 0.6rem;
padding-bottom: 0.6rem;
}
}
< / style >
< / style >
< / head >
< / head >
< body class = "passport-authorize" >
< body class = "passport-authorize" >
< div class = "container" >
@php
< div class = "row justify-content-center" >
$user = request()->user();
< div class = "col-md-6" >
< div class = "text-center mb-5" >
$scopeIcons = [
< img src = "/img/pixelfed-icon-grey.svg" >
'read' => 'fas fa-eye',
'write' => 'fas fa-pen',
'follow' => 'fas fa-user-plus',
'push' => 'fas fa-bell',
'admin:read' => 'fas fa-server',
'admin:read:domain_blocks' => 'fas fa-globe',
'admin:write' => 'fas fa-tools',
'admin:write:domain_blocks' => 'fas fa-gavel',
'security:read' => 'fas fa-shield-alt',
'security:write' => 'fas fa-key',
];
$requestedScopes = collect($scopes);
$grantableScopes = $user->is_admin
? $requestedScopes
: $requestedScopes->reject(fn ($scope) => \App\Passport\ScopeRepository::isAdminScope($scope->id));
$droppedAdminScopes = $requestedScopes->count() - $grantableScopes->count();
$scopeLevel = function ($id) {
if (\App\Passport\ScopeRepository::isAdminScope($id)) {
return 'danger';
}
if (in_array($id, ['write', 'security:write'])) {
return 'warning';
}
return 'default';
};
$levelRank = ['danger' => 0, 'warning' => 1, 'default' => 2];
$sortedScopes = $grantableScopes
->sortBy(fn ($scope) => $levelRank[$scopeLevel($scope->id)])
->values();
$visibleCount = 3;
$hiddenCount = max($sortedScopes->count() - $visibleCount, 0);
$moreLabel = 'Show ' . $hiddenCount . ' more ' . \Illuminate\Support\Str::plural('permission', $hiddenCount);
@endphp
< div class = "authorize-wrap" >
< div class = "text-center mb-4" >
< img src = "/img/pixelfed-icon-grey.svg" width = "48" height = "48" alt = "{{ config_cache('app.name') }}" >
< / div >
< div class = "card authorize-card shadow-sm border-0" >
< div class = "card-body" >
< div class = "handoff" aria-hidden = "true" >
< div class = "handoff-tile handoff-app" > {{ mb_strtoupper(mb_substr($client->name, 0, 1)) }}< / div >
< div class = "handoff-link" > < i class = "fas fa-link" > < / i > < / div >
< img class = "handoff-tile handoff-avatar" src = "{{ $user->profile->avatarUrl() }}" alt = "" >
< / div >
< / div >
< p class = "text-center h3 font-weight-light mb-3" > Authorize {{ $client->name }}< / p >
< div class = "card card-default shadow-none border" >
< h1 class = "h4 font-weight-bold text-center mb-2" > Authorize {{ $client->name }}< / h1 >
< div class = "card-body" >
< p class = "text-muted text-center mb-4" >
< div class = "media" >
{{ $client->name }} wants to access your
< img src = "/img/icon/alert-circle.svg" class = "mr-3" width = "32" height = "32" >
< strong > {{ '@' . $user->username }}< / strong > account.
< div class = "media-body" >
< / p >
< p class = "my-0" > < span class = "font-weight-bold" > {{ $client->name }}< / span > < / p >
< p class = "mb-0 text-muted small" > wants access to your < strong > {{request()->user()->username}}< / strong > account< / p >
@if ($sortedScopes->count() > 0)
< / div >
< p class = "small text-muted mb-2" > This app will be able to:< / p >
< / div >
< hr >
< div class = "scope-list" >
@if (count($scopes) > 0)
@foreach ($sortedScopes as $scope)
< div class = "scopes" >
@if ($loop->index === $visibleCount)
@foreach ($scopes as $scope)
< details class = "scope-more" >
< div class = "media mb-3" >
< summary >
< img src = "/img/icon/unlock.svg" class = "mr-3" width = "32" height = "32" >
< span class = "when-closed" > {{ $moreLabel }}< / span >
< div class = "media-body" >
< span class = "when-open" > Show fewer permissions< / span >
< p class = "my-0" > < span class = "font-weight-bold" > {{ $scope->id }}< / span > < / p >
< i class = "fas fa-chevron-down" > < / i >
< p class = "mb-0 text-muted small" > {{$scope->description}}< / p >
< / summary >
< / div >
< / div >
@endforeach
< / div >
@endif
@endif
< div class = "buttons" >
@php $level = $scopeLevel($scope->id); @endphp
< form method = "post" action = "{{ route('passport.authorizations.approve') }}" >
< div class = "scope-item" >
{{ csrf_field() }}
< div class = "scope-icon {{ $level !== 'default' ? 'scope-icon-' . $level : '' }}" >
< i class = "{{ $scopeIcons[$scope->id] ?? 'fas fa-unlock' }}" > < / i >
< input type = "hidden" name = "state" value = "{{ $request->state }}" >
< / div >
< input type = "hidden" name = "client_id" value = "{{ $client->id }}" >
< div class = "scope-body" >
< input type = "hidden" name = "auth_token" value = "{{ $authToken }}" >
< p class = "scope-desc" > {{ $scope->description }}< / p >
< button type = "submit" class = "btn btn-success font-weight-bold btn-approve" > Authorize< / button >
< span class = "scope-id" > {{ $scope->id }}< / span >
< / form >
< / div >
< form method = "post" action = "{{ route('passport.authorizations.deny') }}" >
{{ csrf_field() }}
{{ method_field('DELETE') }}
< input type = "hidden" name = "state" value = "{{ $request->state }}" >
< input type = "hidden" name = "client_id" value = "{{ $client->id }}" >
< input type = "hidden" name = "auth_token" value = "{{ $authToken }}" >
< button class = "btn btn-outline-danger font-weight-bold" > Cancel< / button >
< / form >
< / div >
< / div >
< hr >
@endforeach
< p class = "mb-0 text-center small text-muted" > Click < a href = "{{ route('logout') }}" class = "font-weight-bold" onclick = "event.preventDefault();document.getElementById('logout_auth').submit();" > here< / a > to log out of this account.< / p >
< form id = "logout_auth" action = "{{ route('logout') }}" method = "POST" style = "display: none;" >
@if ($hiddenCount > 0)
@csrf
< / details >
@endif
< / div >
@endif
@if ($droppedAdminScopes > 0)
< p class = "small text-muted mt-2 mb-0" >
This app also asked for admin permissions, which don't apply to your account.
< / p >
@endif
< div class = "row no-gutters mt-4" >
< div class = "col-6 pr-2" >
< form method = "post" action = "{{ route('passport.authorizations.deny') }}" >
@csrf
@method('DELETE')
< input type = "hidden" name = "state" value = "{{ $request->state }}" >
< input type = "hidden" name = "client_id" value = "{{ $client->id }}" >
< input type = "hidden" name = "auth_token" value = "{{ $authToken }}" >
< button type = "submit" class = "btn btn-outline-secondary btn-block font-weight-bold" > Cancel< / button >
< / form >
< / div >
< div class = "col-6 pl-2" >
< form method = "post" action = "{{ route('passport.authorizations.approve') }}" >
@csrf
< input type = "hidden" name = "state" value = "{{ $request->state }}" >
< input type = "hidden" name = "client_id" value = "{{ $client->id }}" >
< input type = "hidden" name = "auth_token" value = "{{ $authToken }}" >
< button type = "submit" class = "btn btn-primary btn-block font-weight-bold" > Authorize< / button >
< / form >
< / form >
< / div >
< / div >
< / div >
< / div >
< p class = "small text-muted text-center mt-3 mb-0" >
You can revoke this access at any time from your account settings.
< / p >
< / div >
< / div >
< / div >
< / div >
< p class = "small text-muted text-center mt-4 mb-0" >
Signed in as < strong > {{ '@' . $user->username }}< / strong > .
Not you?
< a href = "{{ route('logout') }}" class = "font-weight-bold" onclick = "event.preventDefault();document.getElementById('logout_auth').submit();" > Log out< / a >
< / p >
< form id = "logout_auth" action = "{{ route('logout') }}" method = "POST" class = "d-none" >
@csrf
< / form >
< / div >
< / div >
< / body >
< / body >
< / html >
< / html >