Skip to content

Commit

Permalink
feat: moderation of linked accounts (#73)
Browse files Browse the repository at this point in the history
* feat: moderation of linked accounts

* Apply fixes from StyleCI

* chore: export components

---------

Co-authored-by: StyleCI Bot <[email protected]>
  • Loading branch information
imorland and StyleCIBot committed Feb 13, 2024
1 parent ddd029f commit 186b537
Show file tree
Hide file tree
Showing 16 changed files with 173 additions and 42 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ By default these providers are included:
- LinkedIn
- Twitter

### Permissions

This extension provides the ability to view the status of linked OAuth providers (intended for admin and/or moderator use). In order for this to function correctly, you must also set the permission `Moderate Access Tokens` to at least the same group as you require for `Moderate user's linked accounts`.

### Additional providers

Additional OAuth providers are available for this extension. Here's a handy list of known extensions, let us know if you know of any more and we'll get them added!
Expand Down
10 changes: 2 additions & 8 deletions extend.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,14 @@
->get('/auth/twitter', 'auth.twitter', Controllers\TwitterAuthController::class),

(new Extend\Routes('api'))
->get('/linked-accounts', 'users.provider.list', Api\Controllers\ListProvidersController::class)
->get('/users/{id}/linked-accounts', 'users.provider.list', Api\Controllers\ListProvidersController::class)
->delete('/linked-accounts/{id}', 'users.provider.delete', Api\Controllers\DeleteProviderLinkController::class),

(new Extend\ServiceProvider())
->register(OAuthServiceProvider::class),

(new Extend\ApiSerializer(ForumSerializer::class))
->attributes(function (ForumSerializer $serializer, $model, array $attributes): array {
if ($serializer->getActor()->isGuest()) {
$attributes['fof-oauth'] = resolve('fof-oauth.providers.forum');
}

return $attributes;
}),
->attributes(Api\AddForumAttributes::class),

(new Extend\Settings())
->default('fof-oauth.only_icons', false)
Expand Down
13 changes: 11 additions & 2 deletions js/src/admin/index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import app from 'flarum/admin/app';

import AuthSettingsPage from './components/AuthSettingsPage';
import ConfigureWithOAuthPage from './components/ConfigureWithOAuthPage';
import ConfigureWithOAuthButton from './components/ConfigureWithOAuthButton';

app.initializers.add('fof/oauth', () => {
app.extensionData.for('fof-oauth').registerPage(AuthSettingsPage);
app.extensionData
.for('fof-oauth')
.registerPage(AuthSettingsPage)
.registerPermission(
{
icon: 'fas fa-sign-in-alt',
label: app.translator.trans('fof-oauth.admin.permissions.moderate_user_providers'),
permission: 'moderateUserProviders',
},
'moderate'
);
});

export { AuthSettingsPage, ConfigureWithOAuthPage, ConfigureWithOAuthButton };
10 changes: 5 additions & 5 deletions js/src/forum/components/LinkStatus.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import User from 'flarum/common/models/User';
import ProviderInfo from './ProviderInfo';
import extractText from 'flarum/common/utils/extractText';

interface IAttrs {
interface IAttrs extends ComponentAttrs {
provider: LinkedAccount;
user: User;
}
Expand All @@ -18,7 +18,7 @@ export default class LinkStatus extends Component<IAttrs> {
loading: false,
};

onbeforeupdate(vnode: Mithril.Vnode<ComponentAttrs, this>) {
onbeforeupdate(vnode: Mithril.Vnode<IAttrs, this>) {
super.onbeforeupdate(vnode);
if (app.fof_oauth_linkingInProgress && app.fof_oauth_linkingProvider === this.attrs.provider.name()) {
this.state.loading = true;
Expand All @@ -29,7 +29,7 @@ export default class LinkStatus extends Component<IAttrs> {
}
}

view(vnode: Mithril.Vnode<ComponentAttrs, this>): Mithril.Children {
view(vnode: Mithril.Vnode<IAttrs, this>): Mithril.Children {
return (
<div className={`LinkedAccounts-Account LinkedAccounts-Account--${this.attrs.provider.name()}`}>
{this.iconView()}
Expand Down Expand Up @@ -70,7 +70,7 @@ export default class LinkStatus extends Component<IAttrs> {
</Button>
</div>
);
} else if (!provider.orphaned()) {
} else if (!provider.orphaned() && (user.id() === app.session.user?.id() || !app.forum.attribute<boolean>('fofOauthModerate'))) {
return (
<div className="LinkedAccountsList-item-actions">
<Button
Expand Down Expand Up @@ -101,7 +101,7 @@ export default class LinkStatus extends Component<IAttrs> {
) {
this.state.loading = true;
await provider.delete();
await app.store.find<LinkedAccount[]>('linked-accounts', {});
await app.store.find<LinkedAccount[]>('users/' + this.attrs.user.id() + '/linked-accounts', {});
this.state.loading = false;
m.redraw();
}
Expand Down
2 changes: 1 addition & 1 deletion js/src/forum/components/LinkedAccounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default class LinkedAccounts extends Component<IAttrs, IState> {
}

async loadLinkedAccounts() {
await app.store.find<LinkedAccount[]>('linked-accounts', {});
await app.store.find<LinkedAccount[]>('users/' + this.attrs.user.id() + '/linked-accounts', {});
this.state.loadingAdditional = false;
m.redraw();
}
Expand Down
37 changes: 24 additions & 13 deletions js/src/forum/components/ProviderInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import Component from 'flarum/common/Component';
import type LinkedAccount from '../models/LinkedAccount';
import type Mithril from 'mithril';
import humanTime from 'flarum/common/helpers/humanTime';
import LabelValue from 'flarum/common/components/LabelValue';
import ItemList from 'flarum/common/utils/ItemList';

interface IProviderInfoAttrs {
provider: LinkedAccount;
Expand All @@ -17,7 +19,7 @@ export default class ProviderInfo extends Component<IProviderInfoAttrs> {
<div>
<p className="LinkedAccountsList-item-title">{provider.name()}</p>
<p className="helpText">{app.translator.trans('fof-oauth.forum.user.settings.linked-account.orphaned-account')}</p>
{this.renderDates(provider)}
<div className="LinkedAccountsList">{this.providerInfoItems(provider).toArray()}</div>
</div>
);
}
Expand All @@ -26,7 +28,7 @@ export default class ProviderInfo extends Component<IProviderInfoAttrs> {
return (
<div>
<p className="LinkedAccountsList-item-title">{app.translator.trans(`fof-oauth.forum.providers.${provider.name()}`)}</p>
{this.renderDates(provider)}
<div className="LinkedAccountsList">{this.providerInfoItems(provider).toArray()}</div>
</div>
);
}
Expand All @@ -38,18 +40,27 @@ export default class ProviderInfo extends Component<IProviderInfoAttrs> {
);
}

/**
* Render the created and last used dates for a provider.
*/
renderDates(provider: LinkedAccount): Mithril.Children {
return (
<dl>
<dt className="LinkedAccountsList-item-title">{app.translator.trans('fof-oauth.forum.user.settings.linked-account.link-created-label')}</dt>
<dd className="LinkedAccountsList-item-value">{humanTime(provider.firstLogin())}</dd>
providerInfoItems(provider: LinkedAccount): ItemList<Mithril.Children> {
const items = new ItemList<Mithril.Children>();

items.add(
'firstLogin',
<LabelValue
label={app.translator.trans('fof-oauth.forum.user.settings.linked-account.link-created-label')}
value={humanTime(provider.firstLogin())}
/>,
100
);

<dt className="LinkedAccountsList-item-title">{app.translator.trans('fof-oauth.forum.user.settings.linked-account.last-used-label')}</dt>
<dd className="LinkedAccountsList-item-value">{humanTime(provider.lastLogin())}</dd>
</dl>
items.add(
'lastLogin',
<LabelValue
label={app.translator.trans('fof-oauth.forum.user.settings.linked-account.last-used-label')}
value={humanTime(provider.lastLogin())}
/>,
90
);

return items;
}
}
9 changes: 9 additions & 0 deletions js/src/forum/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import LinkStatus from './LinkStatus';
import LinkedAccounts from './LinkedAccounts';
import ProviderInfo from './ProviderInfo';

export const components = {
ProviderInfo,
LinkStatus,
LinkedAccounts,
};
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type Mithril from 'mithril';

export default function addLinkedAccountsToUserSecurityPage() {
extend(UserSecurityPage.prototype, 'settingsItems', function (items: ItemList<Mithril.Children>) {
if (this.user !== app.session.user) {
if (this.user !== app.session.user && !app.forum.attribute('fofOauthModerate')) {
return;
}

Expand Down
2 changes: 2 additions & 0 deletions js/src/forum/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import extendLoginSignup from './extenders/extendLoginSignup';

export { default as extend } from './extend';

export * from './components';

app.initializers.add('fof/oauth', () => {
extendLoginSignup();
addLinkedAccountsToUserSecurityPage();
Expand Down
1 change: 1 addition & 0 deletions js/src/forum/models/LinkedAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default class LinkedAccount extends Model {
providerIdentifier() {
return Model.attribute<string>('providerIdentifier').call(this);
}

firstLogin() {
return Model.attribute('firstLogin', Model.transformDate).call(this);
}
Expand Down
3 changes: 3 additions & 0 deletions resources/locale/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ fof-oauth:
configure_button_label: Configure with FoF OAuth
settings_accessibility_label: "{name} settings"

permissions:
moderate_user_providers: Moderate user's linked accounts

settings:
advanced:
heading: Advanced
Expand Down
28 changes: 28 additions & 0 deletions src/Api/AddForumAttributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

/*
* This file is part of fof/oauth.
*
* Copyright (c) FriendsOfFlarum.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace FoF\OAuth\Api;

use Flarum\Api\Serializer\ForumSerializer;

class AddForumAttributes
{
public function __invoke(ForumSerializer $serializer, $model, array $attributes): array
{
if ($serializer->getActor()->isGuest()) {
$attributes['fof-oauth'] = resolve('fof-oauth.providers.forum');
} else {
$attributes['fofOauthModerate'] = $serializer->getActor()->can('moderateUserProviders');
}

return $attributes;
}
}
18 changes: 13 additions & 5 deletions src/Api/Controllers/DeleteProviderLinkController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
namespace FoF\OAuth\Api\Controllers;

use Flarum\Api\Controller\AbstractDeleteController;
use Flarum\Foundation\ValidationException;
use Flarum\Http\RequestUtil;
use Flarum\User\LoginProvider;
use Flarum\User\UserRepository;
use FoF\OAuth\Events\UnlinkingFromProvider;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
Expand All @@ -28,9 +28,15 @@ class DeleteProviderLinkController extends AbstractDeleteController
*/
protected $events;

public function __construct(Dispatcher $events)
/**
* @var UserRepository
*/
protected $users;

public function __construct(Dispatcher $events, UserRepository $users)
{
$this->events = $events;
$this->users = $users;
}

/**
Expand All @@ -45,11 +51,13 @@ protected function delete(ServerRequestInterface $request)

$provider = LoginProvider::findOrFail($id);

if ($provider->user_id !== $actor->id) {
throw new ValidationException(['provider' => 'This provider does not belong to you.']);
$user = $this->users->findOrFail($provider->user_id);

if ($user->id !== $actor->id) {
$actor->assertCan('moderateUserProviders');
}

$this->events->dispatch(new UnlinkingFromProvider($actor, $provider));
$this->events->dispatch(new UnlinkingFromProvider($user, $provider, $actor));

$provider->delete();

Expand Down
15 changes: 11 additions & 4 deletions src/Api/Controllers/ListProvidersController.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,25 +46,32 @@ public function __construct(UserRepository $users)
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = RequestUtil::getActor($request);
$actor->assertRegistered();

$user = $this->users->findOrFail(Arr::get($request->getQueryParams(), 'id'));

if ($actor->id !== $user->id) {
$actor->assertCan('moderateUserProviders');
}

$providers = $this->getProviders();

$loginProviders = $this->getUserProviders($actor, $providers);
$loginProviders = $this->getUserProviders($user, $providers);

$data = new Collection();

$providers->each(function (array $provider) use ($loginProviders, &$data, $actor) {
$providers->each(function (array $provider) use ($loginProviders, &$data, $user) {
$loginProvider = $loginProviders->where('provider', Arr::get($provider, 'name'))->first();
$data->add(LoginProviderStatus::build(
Arr::get($provider, 'name'),
Arr::get($provider, 'icon'),
Arr::get($provider, 'priority'),
$actor,
$user,
$loginProvider
));
});

$this->getOrphanedUserProviders($actor, $providers)->each(function (LoginProvider $loginProvider) use (&$data) {
$this->getOrphanedUserProviders($user, $providers)->each(function (LoginProvider $loginProvider) use (&$data) {
$data->add(LoginProviderStatus::build(
$loginProvider->provider,
'fas fa-question',
Expand Down
8 changes: 7 additions & 1 deletion src/Events/UnlinkingFromProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ class UnlinkingFromProvider
*/
public $provider;

/**
* @var User|null
*/
public $actor;

/**
* @param User $user
* @param LoginProvider $provider
*/
public function __construct(User $user, LoginProvider $provider)
public function __construct(User $user, LoginProvider $provider, User $actor = null)
{
$this->user = $user;
$this->provider = $provider;
$this->actor = $actor;
}
}
Loading

0 comments on commit 186b537

Please sign in to comment.