diff --git a/.env.example b/.env.example index 9a43b525a22..52f2e014eda 100644 --- a/.env.example +++ b/.env.example @@ -112,6 +112,11 @@ PUSHER_SECRET= # GITHUB_TOKEN= +# GitHub client for users to associate their GitHub accounts +# Use "/home/account/github-users/callback" for the "Authorization callback URL" field on GitHub +# GITHUB_CLIENT_ID= +# GITHUB_CLIENT_SECRET= + # DATADOG_ENABLED=true # DATADOG_PREFIX=osu.web # DATADOG_API_KEY= diff --git a/app/Http/Controllers/Account/GithubUsersController.php b/app/Http/Controllers/Account/GithubUsersController.php new file mode 100644 index 00000000000..a20c2f4b5ec --- /dev/null +++ b/app/Http/Controllers/Account/GithubUsersController.php @@ -0,0 +1,118 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +namespace App\Http\Controllers\Account; + +use App\Http\Controllers\Controller; +use App\Models\GithubUser; +use League\OAuth2\Client\Provider\Exception\GithubIdentityProviderException; +use League\OAuth2\Client\Provider\Github as GithubProvider; + +class GithubUsersController extends Controller +{ + public function __construct() + { + $this->middleware('auth'); + $this->middleware('verify-user'); + + parent::__construct(); + } + + public function callback() + { + $params = get_params(request()->all(), null, [ + 'code:string', + 'error:string', + 'state:string', + ], ['null_missing' => true]); + + abort_if($params['state'] === null, 422, 'Missing state parameter.'); + abort_unless( + hash_equals(session()->pull('github_auth_state', ''), $params['state']), + 403, + 'Invalid state.', + ); + + // If the user denied authorization on GitHub, redirect back to the GitHub account settings + // + if ($params['error'] === 'access_denied') { + return redirect(route('account.edit').'#github'); + } + + abort_if($params['error'] !== null, 500, 'Error obtaining authorization from GitHub.'); + abort_if($params['code'] === null, 422, 'Missing code parameter.'); + + try { + $token = $this + ->makeGithubOAuthProvider() + ->getAccessToken('authorization_code', ['code' => $params['code']]); + } catch (GithubIdentityProviderException $exception) { + switch ($exception->getMessage()) { + // + case 'bad_verification_code': + return abort(422, 'Invalid authorization code.'); + + // + case 'unverified_user_email': + return abort(422, osu_trans('accounts.github_user.error.unverified_email')); + + default: + throw $exception; + } + } + + $client = new \Github\Client(); + $client->authenticate($token->getToken(), \Github\AuthMethod::ACCESS_TOKEN); + $apiUser = $client->currentUser()->show(); + + $githubUser = GithubUser::firstWhere('canonical_id', $apiUser['id']); + + abort_if($githubUser === null, 422, osu_trans('accounts.github_user.error.no_contribution')); + abort_if($githubUser->user_id !== null, 422, osu_trans('accounts.github_user.error.already_linked')); + + $githubUser->update([ + 'user_id' => auth()->id(), + 'username' => $apiUser['login'], + ]); + + return redirect(route('account.edit').'#github'); + } + + public function create() + { + abort_unless(GithubUser::canAuthenticate(), 404); + + if (auth()->user()->githubUser()->exists()) { + return redirect(route('account.edit').'#github'); + } + + $provider = $this->makeGithubOAuthProvider(); + $url = $provider->getAuthorizationUrl([ + 'allow_signup' => 'false', + 'scope' => ' ', // Provider doesn't support empty scope + ]); + + session()->put('github_auth_state', $provider->getState()); + + return redirect($url); + } + + public function destroy() + { + auth()->user()->githubUser()->update(['user_id' => null]); + + return response(null, 204); + } + + private function makeGithubOAuthProvider(): GithubProvider + { + return new GithubProvider([ + 'clientId' => config('osu.github.client_id'), + 'clientSecret' => config('osu.github.client_secret'), + ]); + } +} diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 7d66b37ef81..eb5b75ce9ee 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -13,6 +13,7 @@ use App\Libraries\UserVerificationState; use App\Mail\UserEmailUpdated; use App\Mail\UserPasswordUpdated; +use App\Models\GithubUser; use App\Models\OAuth\Client; use App\Models\UserAccountHistory; use App\Models\UserNotificationOption; @@ -124,10 +125,15 @@ public function edit() $notificationOptions = $user->notificationOptions->keyBy('name'); + $githubUser = GithubUser::canAuthenticate() && $user->githubUser !== null + ? json_item($user->githubUser, 'GithubUser') + : null; + return ext_view('accounts.edit', compact( 'authorizedClients', 'blocks', 'currentSessionId', + 'githubUser', 'legacyApiKeyJson', 'legacyIrcKeyJson', 'notificationOptions', diff --git a/app/Http/Controllers/ChangelogController.php b/app/Http/Controllers/ChangelogController.php index 51580b3aba3..90a8c55f122 100644 --- a/app/Http/Controllers/ChangelogController.php +++ b/app/Http/Controllers/ChangelogController.php @@ -117,9 +117,10 @@ private static function changelogEntryMessageIncludes(?array $formats): array * "major": true, * "created_at": "2021-06-19T08:09:39+00:00", * "github_user": { - * "id": 218, * "display_name": "bdach", * "github_url": "https://github.com/bdach", + * "github_username": "bdach", + * "id": 218, * "osu_username": null, * "user_id": null, * "user_url": null @@ -318,9 +319,10 @@ public function show($version) * "major": true, * "created_at": "2021-05-20T10:56:49+00:00", * "github_user": { - * "id": null, * "display_name": "peppy", * "github_url": null, + * "github_username": null, + * "id": null, * "osu_username": "peppy", * "user_id": 2, * "user_url": "https://osu.ppy.sh/users/2" diff --git a/app/Models/GithubUser.php b/app/Models/GithubUser.php index b293e7fbf83..c281f877dcb 100644 --- a/app/Models/GithubUser.php +++ b/app/Models/GithubUser.php @@ -3,75 +3,91 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. +declare(strict_types=1); + namespace App\Models; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; + /** - * @property int|null $canonical_id - * @property \Illuminate\Database\Eloquent\Collection $changelogEntries ChangelogEntry + * Note: `$canonical_id`, `$id`, and `$username` are null when this model is + * created for use in legacy changelog entries. + * + * @property int $canonical_id + * @property-read \Illuminate\Database\Eloquent\Collection $changelogEntries * @property \Carbon\Carbon|null $created_at + * @property-read string|null $created_at_json * @property int $id * @property \Carbon\Carbon|null $updated_at - * @property User $user + * @property-read string|null $updated_at_json + * @property-read User|null $user * @property int|null $user_id - * @property string|null $username + * @property string $username */ class GithubUser extends Model { - public static function importFromGithub($data) + /** + * Check if the app is capable of authenticating users via the GitHub API. + */ + public static function canAuthenticate(): bool { - $githubUser = static::where('canonical_id', '=', $data['id'])->first(); - - if (isset($githubUser)) { - $githubUser->update(['username' => $data['login']]); - } else { - $githubUser = static::where('username', '=', $data['login'])->last(); - - if (isset($githubUser)) { - $githubUser->update(['canonical_id' => $data['id']]); - } else { - $githubUser = static::create([ - 'canonical_id' => $data['id'], - 'username' => $data['login'], - ]); - } - } - - return $githubUser; + return config('osu.github.client_id') !== null + && config('osu.github.client_secret') !== null; } - public function user() + /** + * Create or update a GitHub user with data from the GitHub API. + */ + public static function importFromGithub(array $apiUser): static { - return $this->belongsTo(User::class, 'user_id'); + return static::updateOrCreate( + ['canonical_id' => $apiUser['id']], + [ + 'canonical_id' => $apiUser['id'], + 'username' => $apiUser['login'], + ], + ); } - public function changelogEntries() + public function changelogEntries(): HasMany { return $this->hasMany(ChangelogEntry::class); } - public function displayName() + public function user(): BelongsTo { - return presence($this->username) - ?? optional($this->user)->username - ?? '[no name]'; + return $this->belongsTo(User::class, 'user_id'); } - public function githubUrl() + public function displayUsername(): string { - if (present($this->username)) { - return "https://github.com/{$this->username}"; - } + return $this->username ?? $this->user?->username ?? '[no name]'; } - public function userUrl() + public function githubUrl(): ?string { - if ($this->user_id !== null) { - return route('users.show', $this->user_id); - } + return $this->username !== null + ? "https://github.com/{$this->username}" + : null; } - public function url() + public function getAttribute($key) { - return $this->githubUrl() ?? $this->userUrl(); + return match ($key) { + 'canonical_id', + 'id', + 'user_id', + 'username' => $this->getRawAttribute($key), + + 'created_at', + 'updated_at' => $this->getTimeFast($key), + + 'created_at_json', + 'updated_at_json' => $this->getJsonTimeFast($key), + + 'changelogEntries', + 'user' => $this->getRelationValue($key), + }; } } diff --git a/app/Models/User.php b/app/Models/User.php index d16c18d2de7..74a1faad7ac 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -67,7 +67,7 @@ * @property-read Collection $follows * @property-read Collection $forumPosts * @property-read Collection $friends - * @property-read Collection $githubUsers + * @property-read GithubUser|null $githubUser * @property-read Collection $givenKudosu * @property int $group_id * @property bool $hide_presence @@ -881,7 +881,7 @@ public function getAttribute($key) 'follows', 'forumPosts', 'friends', - 'githubUsers', + 'githubUser', 'givenKudosu', 'legacyIrcKey', 'monthlyPlaycounts', @@ -1166,9 +1166,9 @@ public function badges() return $this->hasMany(UserBadge::class); } - public function githubUsers() + public function githubUser(): HasOne { - return $this->hasMany(GithubUser::class); + return $this->hasOne(GithubUser::class); } public function legacyIrcKey(): HasOne diff --git a/app/Transformers/GithubUserTransformer.php b/app/Transformers/GithubUserTransformer.php index 14470631888..55300878bbe 100644 --- a/app/Transformers/GithubUserTransformer.php +++ b/app/Transformers/GithubUserTransformer.php @@ -3,21 +3,24 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. +declare(strict_types=1); + namespace App\Transformers; use App\Models\GithubUser; class GithubUserTransformer extends TransformerAbstract { - public function transform(GithubUser $githubUser) + public function transform(GithubUser $githubUser): array { return [ - 'id' => $githubUser->getKey(), - 'display_name' => $githubUser->displayName(), + 'display_name' => $githubUser->displayUsername(), 'github_url' => $githubUser->githubUrl(), - 'osu_username' => optional($githubUser->user)->username, + 'github_username' => $githubUser->username, + 'id' => $githubUser->getKey(), + 'osu_username' => $githubUser->user?->username, 'user_id' => $githubUser->user_id, - 'user_url' => $githubUser->userUrl(), + 'user_url' => $githubUser->user?->url(), ]; } } diff --git a/composer.json b/composer.json index 092e8687b68..8174d3bd1d4 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "league/commonmark": "^2.0", "league/flysystem-aws-s3-v3": "*", "league/fractal": "*", + "league/oauth2-github": "^3.1", "league/oauth2-server": "^8.3", "maennchen/zipstream-php": "^2.1", "mariuzzo/laravel-js-localization": "*", diff --git a/composer.lock b/composer.lock index 995b52bb2d1..5d950fd680d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9763633a7cacc613aaf6e9c7e00f5565", + "content-hash": "21e844181566fff38f8e9aa1048b0732", "packages": [ { "name": "anhskohbo/no-captcha", @@ -4151,6 +4151,142 @@ ], "time": "2022-04-17T13:12:02+00:00" }, + { + "name": "league/oauth2-client", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-client.git", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8", + "reference": "160d6274b03562ebeb55ed18399281d8118b76c8", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0 || ^7.0", + "paragonie/random_compat": "^1 || ^2 || ^9.99", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.5", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5", + "squizlabs/php_codesniffer": "^2.3 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Woody Gilk", + "homepage": "https://github.com/shadowhand", + "role": "Contributor" + } + ], + "description": "OAuth 2.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "identity", + "idp", + "oauth", + "oauth2", + "single sign on" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-client/issues", + "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0" + }, + "time": "2023-04-16T18:19:15+00:00" + }, + { + "name": "league/oauth2-github", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-github.git", + "reference": "97f31cd70e76f81e8f5b4e2ab6f3708e2db7ac18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-github/zipball/97f31cd70e76f81e8f5b4e2ab6f3708e2db7ac18", + "reference": "97f31cd70e76f81e8f5b4e2ab6f3708e2db7ac18", + "shasum": "" + }, + "require": { + "ext-json": "*", + "league/oauth2-client": "^2.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.4", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth2\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steven Maguire", + "email": "stevenmaguire@gmail.com", + "homepage": "https://github.com/stevenmaguire" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://github.com/shadowhand" + } + ], + "description": "Github OAuth 2.0 Client Provider for The PHP League OAuth2-Client", + "keywords": [ + "authorisation", + "authorization", + "client", + "github", + "oauth", + "oauth2" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-github/issues", + "source": "https://github.com/thephpleague/oauth2-github/tree/3.1.0" + }, + "time": "2022-11-04T14:01:49+00:00" + }, { "name": "league/oauth2-server", "version": "8.5.3", diff --git a/config/osu.php b/config/osu.php index 9aa643685af..c5e12c1993b 100644 --- a/config/osu.php +++ b/config/osu.php @@ -124,6 +124,10 @@ 'git-sha' => presence(env('GIT_SHA')) ?? (file_exists(__DIR__.'/../version') ? trim(file_get_contents(__DIR__.'/../version')) : null) ?? 'unknown-version', + 'github' => [ + 'client_id' => presence(env('GITHUB_CLIENT_ID')), + 'client_secret' => presence(env('GITHUB_CLIENT_SECRET')), + ], 'is_development_deploy' => get_bool(env('IS_DEVELOPMENT_DEPLOY')) ?? true, 'landing' => [ 'video_url' => env('LANDING_VIDEO_URL', 'https://assets.ppy.sh/media/landing.mp4'), diff --git a/database/migrations/2023_09_25_230000_update_github_users.php b/database/migrations/2023_09_25_230000_update_github_users.php new file mode 100644 index 00000000000..d13200fe68f --- /dev/null +++ b/database/migrations/2023_09_25_230000_update_github_users.php @@ -0,0 +1,39 @@ +. Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +declare(strict_types=1); + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +return new class extends Migration +{ + /** + * Run the migrations. + */ + public function up(): void + { + Schema::table('github_users', function (Blueprint $table): void { + $table->unsignedBigInteger('canonical_id')->change(); + $table->string('username')->change(); + + $table->unique('user_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('github_users', function (Blueprint $table): void { + $table->unsignedBigInteger('canonical_id')->nullable()->change(); + $table->string('username')->nullable()->change(); + + $table->dropUnique('github_users_user_id_unique'); + }); + } +}; diff --git a/resources/css/bem-index.less b/resources/css/bem-index.less index 71b4044667c..9e444ce6183 100644 --- a/resources/css/bem-index.less +++ b/resources/css/bem-index.less @@ -177,6 +177,7 @@ @import "bem/gallery-thumbnails"; @import "bem/game-mode"; @import "bem/game-mode-link"; +@import "bem/github-user"; @import "bem/grid-items"; @import "bem/header-buttons"; @import "bem/header-nav-mobile"; diff --git a/resources/css/bem/account-edit-entry.less b/resources/css/bem/account-edit-entry.less index 8c492c3da07..0767c63d6b7 100644 --- a/resources/css/bem/account-edit-entry.less +++ b/resources/css/bem/account-edit-entry.less @@ -21,7 +21,7 @@ flex-wrap: nowrap; } - &--avatar { + &--block { display: block; @media @desktop { diff --git a/resources/css/bem/btn-osu-big.less b/resources/css/bem/btn-osu-big.less index b9f48928a93..95dc76ecdb8 100644 --- a/resources/css/bem/btn-osu-big.less +++ b/resources/css/bem/btn-osu-big.less @@ -73,6 +73,10 @@ position: relative; } + &--account-edit-small { + min-width: 70px; + } + &--artist-track-search { --disabled-bg: hsl(var(--hsl-b2)); } @@ -303,10 +307,6 @@ margin: 0 5px; } - &--settings-oauth { - min-width: 70px; - } - &--store-action { display: block; width: max-content; diff --git a/resources/css/bem/github-user.less b/resources/css/bem/github-user.less new file mode 100644 index 00000000000..0f702876eb6 --- /dev/null +++ b/resources/css/bem/github-user.less @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +.github-user { + display: flex; + align-items: center; + justify-content: space-between; + + &__name { + font-size: @font-size--title-small-3; + } +} diff --git a/resources/js/entrypoints/account-edit.tsx b/resources/js/entrypoints/account-edit.tsx index 4f203b7ece6..e134e8825d2 100644 --- a/resources/js/entrypoints/account-edit.tsx +++ b/resources/js/entrypoints/account-edit.tsx @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. // See the LICENCE file in the repository root for full licence text. +import GithubUser from 'github-user'; import { ClientJson } from 'interfaces/client-json'; import { OwnClientJson } from 'interfaces/own-client-json'; import LegacyApiKey from 'legacy-api-key'; @@ -20,6 +21,10 @@ core.reactTurbolinks.register('authorized-clients', () => { return ; }); +core.reactTurbolinks.register('github-user', (container: HTMLElement) => ( + +)); + core.reactTurbolinks.register('legacy-api-key', (container: HTMLElement) => ( )); diff --git a/resources/js/github-user/index.tsx b/resources/js/github-user/index.tsx new file mode 100644 index 00000000000..a71016171f9 --- /dev/null +++ b/resources/js/github-user/index.tsx @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +import BigButton from 'components/big-button'; +import GithubUserJson from 'interfaces/github-user-json'; +import { route } from 'laroute'; +import { action, makeObservable, observable, reaction } from 'mobx'; +import { disposeOnUnmount, observer } from 'mobx-react'; +import * as React from 'react'; +import { onErrorWithCallback } from 'utils/ajax'; +import { trans } from 'utils/lang'; + +interface Props { + container: HTMLElement; +} + +@observer +export default class GithubUser extends React.Component { + @observable private unlinkXhr: JQuery.jqXHR | null = null; + @observable private user; + + constructor(props: Props) { + super(props); + + this.user = JSON.parse(this.props.container.dataset.user ?? '') as GithubUserJson | null; + + makeObservable(this); + + disposeOnUnmount(this, reaction( + () => JSON.stringify(this.user), + (githubUserJson) => this.props.container.dataset.user = githubUserJson, + )); + } + + componentWillUnmount() { + this.unlinkXhr?.abort(); + } + + render() { + return ( +
+ {this.user != null ? ( + + ) : ( + <> + +
+ {trans('accounts.github_user.info')} +
+ + )} +
+ ); + } + + @action + private readonly onUnlinkButtonClick = () => { + if (this.unlinkXhr != null) return; + + this.unlinkXhr = $.ajax( + route('account.github-users.destroy'), + { method: 'DELETE' }, + ) + .done(action(() => this.user = null)) + .fail(onErrorWithCallback(this.onUnlinkButtonClick)) + .always(action(() => this.unlinkXhr = null)); + }; +} diff --git a/resources/js/interfaces/github-user-json.ts b/resources/js/interfaces/github-user-json.ts new file mode 100644 index 00000000000..5734a11edcd --- /dev/null +++ b/resources/js/interfaces/github-user-json.ts @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. +// See the LICENCE file in the repository root for full licence text. + +interface CommonProps { + display_name: string; +} + +interface GithubProps { + github_url: string; + github_username: string; +} + +interface IdProps { + id: number; +} + +interface OsuProps { + osu_username: string; + user_id: number; + user_url: string; +} + +type Null = Record; + +type GithubUserJson = CommonProps & GithubProps & IdProps & (OsuProps | Null); +type Legacy = CommonProps & Null & Null & OsuProps; +type Placeholder = CommonProps & GithubProps & Null & Null; + +export default GithubUserJson; +export type GithubUserJsonForChangelog = GithubUserJson | Legacy | Placeholder; diff --git a/resources/js/legacy-api-key/details.tsx b/resources/js/legacy-api-key/details.tsx index 246e84d045c..e5dadd6ead1 100644 --- a/resources/js/legacy-api-key/details.tsx +++ b/resources/js/legacy-api-key/details.tsx @@ -84,7 +84,7 @@ export default class Details extends React.Component {
{ {
{ { disabled={client.revoked} icon={client.revoked ? 'fas fa-ban' : 'fas fa-trash'} isBusy={client.isRevoking} - modifiers={['account-edit', 'danger', 'settings-oauth']} + modifiers={['account-edit', 'account-edit-small', 'danger']} props={{ onClick: this.revokeClicked, }} diff --git a/resources/js/oauth/own-client.tsx b/resources/js/oauth/own-client.tsx index 2eb78761884..7e537a26c8b 100644 --- a/resources/js/oauth/own-client.tsx +++ b/resources/js/oauth/own-client.tsx @@ -49,7 +49,7 @@ export class OwnClient extends React.Component { { disabled={client.revoked} icon={client.revoked ? 'fas fa-ban' : 'fas fa-trash'} isBusy={client.isRevoking} - modifiers={['account-edit', 'danger', 'settings-oauth']} + modifiers={['account-edit', 'account-edit-small', 'danger']} props={{ onClick: this.deleteClicked, }} diff --git a/resources/lang/en/accounts.php b/resources/lang/en/accounts.php index c09893b3036..cabf97d5793 100644 --- a/resources/lang/en/accounts.php +++ b/resources/lang/en/accounts.php @@ -62,6 +62,19 @@ ], ], + 'github_user' => [ + 'info' => "If you're a contributor to osu!'s open-source repositories, linking your GitHub account here will associate your changelog entries with your osu! profile. GitHub accounts with no contribution history to osu! cannot be linked.", + 'link' => 'Link GitHub Account', + 'title' => 'GitHub', + 'unlink' => 'Unlink GitHub Account', + + 'error' => [ + 'already_linked' => 'This GitHub account is already linked to a different user.', + 'no_contribution' => 'Cannot link GitHub account without any contribution history in osu! repositories.', + 'unverified_email' => 'Please verify your primary email on GitHub, then try linking your account again.', + ], + ], + 'notifications' => [ 'beatmapset_discussion_qualified_problem' => 'receive notifications for new problems on qualified beatmaps of the following modes', 'beatmapset_disqualify' => 'receive notifications for when beatmaps of the following modes are disqualified', diff --git a/resources/views/accounts/_edit_github_user.blade.php b/resources/views/accounts/_edit_github_user.blade.php new file mode 100644 index 00000000000..b95ea94ca66 --- /dev/null +++ b/resources/views/accounts/_edit_github_user.blade.php @@ -0,0 +1,18 @@ +{{-- + Copyright (c) ppy Pty Ltd . Licensed under the GNU Affero General Public License v3.0. + See the LICENCE file in the repository root for full licence text. +--}} + diff --git a/resources/views/accounts/edit.blade.php b/resources/views/accounts/edit.blade.php index 74c2ced25aa..2fbc77a0442 100644 --- a/resources/views/accounts/edit.blade.php +++ b/resources/views/accounts/edit.blade.php @@ -76,7 +76,7 @@