131 lines
4.4 KiB
PHP
131 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Auth;
|
|
|
|
use App\Abstracts\Http\Controller;
|
|
use App\Models\Auth\DingTalkIdentity;
|
|
use App\Models\Auth\DingTalkOAuthState;
|
|
use App\Services\Auth\DingTalkOAuthClient;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Str;
|
|
use Throwable;
|
|
|
|
class DingTalkLogin extends Controller
|
|
{
|
|
public function redirect(DingTalkOAuthClient $client): RedirectResponse
|
|
{
|
|
if (! $client->isConfigured()) {
|
|
return redirect()->route('login')->with('dingtalk_error', trans('auth.dingtalk.unavailable'));
|
|
}
|
|
|
|
DingTalkOAuthState::query()
|
|
->where('expires_at', '<', now()->subDay())
|
|
->delete();
|
|
|
|
$state = Str::random(64);
|
|
DingTalkOAuthState::query()->create([
|
|
'state_hash' => hash('sha256', $state),
|
|
'expires_at' => now()->addMinutes(max(1, (int) config('dingtalk.state_ttl_minutes', 5))),
|
|
]);
|
|
|
|
return redirect()->away($client->authorizationUrl($state));
|
|
}
|
|
|
|
public function callback(Request $request, DingTalkOAuthClient $client): RedirectResponse
|
|
{
|
|
$stateValue = trim((string) $request->query('state', ''));
|
|
$authorizationCode = trim((string) ($request->query('authCode') ?: $request->query('code', '')));
|
|
|
|
if ($stateValue === '' || $authorizationCode === '') {
|
|
return $this->failed();
|
|
}
|
|
|
|
$state = DB::transaction(function () use ($stateValue): ?DingTalkOAuthState {
|
|
$candidate = DingTalkOAuthState::query()
|
|
->where('state_hash', hash('sha256', $stateValue))
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if (! $candidate || $candidate->consumed_at || $candidate->expires_at->isPast()) {
|
|
return null;
|
|
}
|
|
|
|
$candidate->forceFill(['consumed_at' => now()])->save();
|
|
|
|
return $candidate;
|
|
});
|
|
|
|
if (! $state) {
|
|
return $this->failed();
|
|
}
|
|
|
|
try {
|
|
$token = $client->exchangeAuthorizationCode($authorizationCode);
|
|
$profile = $client->currentUser($token['access_token']);
|
|
} catch (Throwable $exception) {
|
|
Log::warning('DingTalk login failed while resolving the authorized user.', [
|
|
'exception' => $exception::class,
|
|
]);
|
|
|
|
return $this->failed();
|
|
}
|
|
|
|
$identity = DingTalkIdentity::query()
|
|
->with('user')
|
|
->where('union_id', $profile['union_id'])
|
|
->first();
|
|
|
|
if ($identity) {
|
|
return $this->loginBoundUser($request, $identity, $profile);
|
|
}
|
|
|
|
$bindingToken = Str::random(64);
|
|
$state->forceFill([
|
|
'binding_token_hash' => hash('sha256', $bindingToken),
|
|
'union_id' => $profile['union_id'],
|
|
'open_id' => $profile['open_id'],
|
|
'nickname' => $profile['nickname'],
|
|
'expires_at' => now()->addMinutes(max(1, (int) config('dingtalk.binding_ttl_minutes', 10))),
|
|
])->save();
|
|
|
|
return redirect()->route('login', ['dingtalk_binding' => $bindingToken])
|
|
->with('dingtalk_binding', trans('auth.dingtalk.confirm_binding'));
|
|
}
|
|
|
|
protected function loginBoundUser(Request $request, DingTalkIdentity $identity, array $profile): RedirectResponse
|
|
{
|
|
$user = $identity->user;
|
|
$company = $user?->withoutEvents(fn () => $user->companies()->enabled()->first());
|
|
|
|
if (! $user || ! $user->enabled || ! $company) {
|
|
Log::info('DingTalk login denied for a disabled or unassigned account.', [
|
|
'identity_id' => $identity->id,
|
|
]);
|
|
|
|
return $this->failed();
|
|
}
|
|
|
|
Auth::login($user, true);
|
|
$request->session()->regenerate();
|
|
|
|
$identity->forceFill([
|
|
'open_id' => $profile['open_id'] ?: $identity->open_id,
|
|
'nickname' => $profile['nickname'] ?: $identity->nickname,
|
|
'last_logged_in_at' => now(),
|
|
])->save();
|
|
|
|
$url = route($user->landing_page, ['company_id' => $company->id]);
|
|
|
|
return redirect()->intended($url);
|
|
}
|
|
|
|
protected function failed(): RedirectResponse
|
|
{
|
|
return redirect()->route('login')->with('dingtalk_error', trans('auth.dingtalk.failed'));
|
|
}
|
|
}
|