Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add account setting to link GitHub account #9465

Merged
merged 56 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ab44045
Clean up GithubUser model and transformer
cl8n Nov 6, 2022
d478ea9
Add User associate option to `GithubUser::importFromGithub()`
cl8n Nov 6, 2022
72dd9ce
Add endpoints to link and unlink GitHub accounts from users
cl8n Nov 6, 2022
63178a8
Add GitHub account options to account edit page
cl8n Nov 6, 2022
7581e68
Only show GitHub menu if client info is set
cl8n Nov 6, 2022
c9a25d9
Docs
cl8n Nov 6, 2022
71d8380
Fix missing returns
cl8n Nov 6, 2022
67e88b0
Check for null canonical_id and username when counting users
cl8n Nov 6, 2022
fe294b2
Expect verification for github-users routes in SanityTest
cl8n Nov 6, 2022
d3b1028
Merge master
cl8n Dec 8, 2022
14033e9
Remove max accounts
cl8n Dec 8, 2022
e5c6c8c
Simpler destroy endpoint
cl8n Dec 8, 2022
0f59df3
Fix trans usage
cl8n Dec 8, 2022
0a5badf
Use league/oauth2-github
cl8n Dec 9, 2022
ec970f3
Only allow GitHub accounts with contribution
cl8n Dec 9, 2022
ffc28fd
canonical_id and username can't be empty
cl8n Dec 9, 2022
d459387
Document how to set callback URL
cl8n Dec 9, 2022
36ef18a
Support linking only one GitHub account
cl8n Dec 9, 2022
8c9a8d6
Merge master
cl8n Dec 9, 2022
6ab5a80
Merge master
cl8n May 19, 2023
465c615
Fix file path for github user component
cl8n May 20, 2023
a90f572
Prevent re-assigning same GitHub account
cl8n May 20, 2023
17fe25a
Fix display of legacy changelog entries
cl8n May 20, 2023
708c5d9
Redate migration
cl8n May 20, 2023
a06ed1c
Fix url method name
cl8n May 20, 2023
69973fa
Merge master, update league/oauth2-github again
cl8n Sep 11, 2023
03f19dc
Fix state issues on navigation
cl8n Sep 12, 2023
a146494
Continue xhr instead of aborting, remove redundant `deleting` state
cl8n Sep 12, 2023
179ebed
Missing action
cl8n Sep 12, 2023
eeaf389
Fix div id removed by accident
cl8n Sep 12, 2023
d711755
Missing turbolinks disable
cl8n Sep 12, 2023
014b21a
Merge settings-github and settings-oauth button styles
cl8n Sep 12, 2023
5f7c7fd
Use timing-safe comparison for state
cl8n Sep 12, 2023
7d07269
Better error handling for GitHub authentication and token requests
cl8n Sep 12, 2023
b2c43b9
Add info message about GitHub accounts and clean up styling
cl8n Sep 12, 2023
2ab50a4
"Unlink" instead of "Delete" on button
cl8n Sep 12, 2023
8b5c078
Correctly type GithubUserJson
cl8n Sep 12, 2023
c42c8d5
Merge master
cl8n Sep 12, 2023
92c9692
Redate migration and update change format
cl8n Sep 12, 2023
a59705f
Better names for unlink handler/xhr
cl8n Sep 12, 2023
5363439
Use `Record` type
cl8n Sep 13, 2023
0d2b5aa
Use switch on exception message
cl8n Sep 25, 2023
f89854a
Better var name for github user
cl8n Sep 25, 2023
f988742
Remove id from destroy route
cl8n Sep 25, 2023
49f4899
No need to sort relation anymore
cl8n Sep 25, 2023
b63bff9
Remove `osuUsername()` and `userUrl()`
cl8n Sep 25, 2023
90d7e84
Use disposeOnUnmount
cl8n Sep 25, 2023
5f2f1c9
Unnecessary type
cl8n Sep 25, 2023
b1c27eb
Title-case buttons
cl8n Sep 25, 2023
86ec932
Add unique key to user_id, remove automatic delete, rename migration
cl8n Sep 25, 2023
e4e3ee5
Even more accurate GithubUserJson type
cl8n Sep 25, 2023
99470d9
Move account-edit-entry block into react, remove div for react
cl8n Sep 25, 2023
99f4f1d
Redirect back to page if already exists, instead of error
cl8n Sep 25, 2023
39bb425
Merge master
cl8n Sep 25, 2023
c245bfe
Readonly
cl8n Sep 25, 2023
a95405b
Merge branch 'master' into github-users
nanaya Sep 27, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ PUSHER_SECRET=

# GITHUB_TOKEN=

# GitHub client for users to associate their GitHub accounts
cl8n marked this conversation as resolved.
Show resolved Hide resolved
# GITHUB_CLIENT_ID=
# GITHUB_CLIENT_SECRET=

# DATADOG_ENABLED=true
# DATADOG_PREFIX=osu.web
# DATADOG_API_KEY=
Expand Down
88 changes: 88 additions & 0 deletions app/Http/Controllers/Account/GithubUsersController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

// Copyright (c) ppy Pty Ltd <[email protected]>. 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 Github\Client as GithubClient;
use GuzzleHttp\Client as HttpClient;

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',
'state:string',
]);

abort_unless(isset($params['code']), 422, 'Invalid code.');
abort_unless(
isset($params['state']) && $params['state'] === session()->pull('github_auth_state'),
cl8n marked this conversation as resolved.
Show resolved Hide resolved
403,
'Invalid state.',
);

$tokenResponseBody = (new HttpClient())
cl8n marked this conversation as resolved.
Show resolved Hide resolved
->request('POST', 'https://github.com/login/oauth/access_token', [
'query' => [
'client_id' => config('osu.github.client_id'),
'client_secret' => config('osu.github.client_secret'),
'code' => $params['code'],
],
])
->getBody()
->getContents();
parse_str($tokenResponseBody, $tokenResponseBodyParams);
$token = $tokenResponseBodyParams['access_token'] ?? null;

abort_if($token === null, 500, 'Invalid response from GitHub API.');

$githubClient = new GithubClient();
$githubClient->authenticate($token, null, GithubClient::AUTH_ACCESS_TOKEN);
$githubApiUser = $githubClient->currentUser()->show();

GithubUser::importFromGithub($githubApiUser, auth()->user());

return redirect(route('account.edit').'#github');
cl8n marked this conversation as resolved.
Show resolved Hide resolved
}

public function create()
{
abort_unless(GithubUser::canAuthenticate(), 404);

$state = bin2hex(random_bytes(32));
session()->put('github_auth_state', $state);

return redirect('https://github.com/login/oauth/authorize?'.http_build_query([
'allow_signup' => 'false',
'client_id' => config('osu.github.client_id'),
'scope' => '',
'state' => $state,
]));
}

public function destroy(int $id)
{
auth()->user()
->githubUsers()
->withGithubInfo()
->findOrFail($id)
->update(['user_id' => null]);

return response(null, 204);
}
}
11 changes: 11 additions & 0 deletions app/Http/Controllers/AccountController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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;
Expand Down Expand Up @@ -113,10 +114,20 @@ public function edit()

$notificationOptions = $user->notificationOptions->keyBy('name');

if (GithubUser::canAuthenticate()) {
$githubUsers = json_collection(
$user->githubUsers()->withGithubInfo()->get(),
cl8n marked this conversation as resolved.
Show resolved Hide resolved
'GithubUser',
);
} else {
$githubUsers = null;
}

return ext_view('accounts.edit', compact(
'authorizedClients',
'blocks',
'currentSessionId',
'githubUsers',
'notificationOptions',
'ownClients',
'sessions'
Expand Down
6 changes: 4 additions & 2 deletions app/Http/Controllers/ChangelogController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand Down
109 changes: 77 additions & 32 deletions app/Models/GithubUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,120 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
* @property int|null $canonical_id
* @property \Illuminate\Database\Eloquent\Collection $changelogEntries ChangelogEntry
* @property-read \Illuminate\Database\Eloquent\Collection<ChangelogEntry> $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
* @method static Builder withGithubInfo()
*/
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
{
return config('osu.github.client_id') !== null
&& config('osu.github.client_secret') !== null;
}

/**
* Create or update a GitHub user with data from the GitHub API. Optionally
* associate the GitHub user to an osu! user.
*/
public static function importFromGithub(array $apiUser, ?User $user = null): static
{
$githubUser = static::where('canonical_id', '=', $data['id'])->first();
$params = [
'canonical_id' => $apiUser['id'],
'username' => $apiUser['login'],
];
if ($user !== null) {
$params['user_id'] = $user->getKey();
}

$githubUser = static::where('canonical_id', $params['canonical_id'])->first()
?? static::where('username', $params['username'])->last();

if (isset($githubUser)) {
$githubUser->update(['username' => $data['login']]);
if ($githubUser === null) {
return static::create($params);
} 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'],
]);
}
$githubUser->update($params);
return $githubUser;
}
}

return $githubUser;
public function changelogEntries(): HasMany
{
return $this->hasMany(ChangelogEntry::class);
}

public function user()
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'user_id');
}

public function changelogEntries()
public function scopeWithGithubInfo(Builder $query): void
{
return $this->hasMany(ChangelogEntry::class);
$query->whereNotNull(['canonical_id', 'username']);
cl8n marked this conversation as resolved.
Show resolved Hide resolved
}

public function displayName()
public function displayName(): string
{
return presence($this->username)
?? optional($this->user)->username
?? $this->osuUsername()
?? '[no name]';
}

public function githubUrl()
public function githubUrl(): ?string
{
if (present($this->username)) {
return "https://github.com/{$this->username}";
}
return present($this->username)
? "https://github.com/{$this->username}"
: null;
}

public function userUrl()
public function osuUsername(): ?string
{
if ($this->user_id !== null) {
return route('users.show', $this->user_id);
}
return $this->user?->username;
}

public function url()
public function userUrl(): ?string
{
return $this->githubUrl() ?? $this->userUrl();
return $this->user_id !== null
? route('users.show', $this->user_id)
: null;
}
cl8n marked this conversation as resolved.
Show resolved Hide resolved

public function getAttribute($key)
{
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),
};
}
}
9 changes: 6 additions & 3 deletions app/Transformers/GithubUserTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,22 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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(),
'github_url' => $githubUser->githubUrl(),
'osu_username' => optional($githubUser->user)->username,
'github_username' => $githubUser->username,
'id' => $githubUser->getKey(),
'osu_username' => $githubUser->osuUsername(),
'user_id' => $githubUser->user_id,
'user_url' => $githubUser->userUrl(),
];
Expand Down
4 changes: 4 additions & 0 deletions config/osu.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,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'),
Expand Down
1 change: 1 addition & 0 deletions resources/assets/less/bem-index.less
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@
@import "bem/gallery-thumbnails";
@import "bem/game-mode";
@import "bem/game-mode-link";
@import "bem/github-user";
@import "bem/grid";
@import "bem/grid-cell";
@import "bem/grid-items";
Expand Down
15 changes: 15 additions & 0 deletions resources/assets/less/bem/github-user.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. 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;
border-bottom: 1px solid @osu-colour-b5;
margin-bottom: 10px;
padding-bottom: 10px;

&__name {
font-size: @font-size--title-small-3;
}
}
Loading