Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -90,3 +90,7 @@ GITHUB_API_TOKEN=
# Dev.to — slug da organização cujos artigos o contents:sync-articles busca.
# A chave de API de cada pessoa é conectada pelo painel (/app/profile → Conexões), não aqui.
DEVTO_ORG_SLUG=he4rt

# API mobile (he4rt-app) — autenticação JWT, gerar com `php artisan jwt:secret`.
JWT_SECRET=
HE4RT_APP_DEEPLINK_SCHEME=he4rtapp
3 changes: 3 additions & 0 deletions .env.testing.example
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

DISCORD_TOKEN=

# API mobile (he4rt-app) — valor fixo, não é segredo de produção.
JWT_SECRET=testing-jwt-secret-do-not-use-in-production
4 changes: 2 additions & 2 deletions app-modules/bot-discord/phpstan.ignore.neon
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ parameters:
count: 1
path: src/Tasks/VoiceExperienceTask.php
-
message: '#^Parameter \#1 \$roles of method Discord\\Parts\\User\\Member::setRoles\(\) expects#'
message: '#^Parameter \#1 \$roles of method Discord\\Parts\\Guild\\Member\\Member::setRoles\(\) expects#'
identifier: argument.type
count: 1
path: src/SlashCommands/CargoDelasCommand.php
-
message: '#^Parameter \#1 \$roles of method Discord\\Parts\\User\\Member::setRoles\(\) expects#'
message: '#^Parameter \#1 \$roles of method Discord\\Parts\\Guild\\Member\\Member::setRoles\(\) expects#'
identifier: argument.type
count: 1
path: src/SlashCommands/IntroductionCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace He4rt\BotDiscord\Actions\Welcome;

use Discord\Parts\User\Member;
use Discord\Parts\Guild\Member\Member;
use He4rt\BotDiscord\DTO\WelcomeContextDTO;
use He4rt\BotDiscord\Enums\DiscordErrorCode;
use He4rt\BotDiscord\Welcome\WelcomeEmbedBuilder;
Expand Down
2 changes: 1 addition & 1 deletion app-modules/bot-discord/src/Concerns/ResolvesGuildIcon.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace He4rt\BotDiscord\Concerns;

use Discord\Parts\User\Member;
use Discord\Parts\Guild\Member\Member;

trait ResolvesGuildIcon
{
Expand Down
2 changes: 1 addition & 1 deletion app-modules/bot-discord/src/DTO/WelcomeContextDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace He4rt\BotDiscord\DTO;

use Discord\Parts\User\Member;
use Discord\Parts\Guild\Member\Member;

final readonly class WelcomeContextDTO
{
Expand Down
2 changes: 1 addition & 1 deletion app-modules/bot-discord/src/Events/WelcomeMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace He4rt\BotDiscord\Events;

use Discord\Discord;
use Discord\Parts\User\Member;
use Discord\Parts\Guild\Member\Member;
use Discord\WebSockets\Event as Events;
use He4rt\BotDiscord\Actions\Welcome\AnnounceNewMemberAction;
use He4rt\BotDiscord\Actions\Welcome\SendWelcomeDmAction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@

namespace He4rt\BotDiscord\SlashCommands;

use Discord\Parts\Guild\Role;
use Discord\Parts\Guild\Member\Member;
use Discord\Parts\Guild\Role\Role;
use Discord\Parts\Interactions\Command\Option;
use Discord\Parts\Interactions\Interaction;
use Discord\Parts\User\Member;

class CargoDelasCommand extends AbstractSlashCommand
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Discord\Builders\Components\TextInput;
use Discord\Helpers\Collection;
use Discord\Parts\Guild\Role;
use Discord\Parts\Guild\Role\Role;
use Discord\Parts\Interactions\Interaction;
use He4rt\Identity\ExternalIdentity\DTOs\ResolveUserProviderDTO;
use He4rt\Identity\ExternalIdentity\Enums\IdentityProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace He4rt\BotDiscord\SlashCommands;

use Discord\Parts\Channel\Channel;
use Discord\Parts\Guild\Role;
use Discord\Parts\Guild\Role\Role;
use Discord\Parts\Interactions\Command\Option;
use Discord\Parts\Interactions\Interaction;
use Exception;
Expand Down
40 changes: 23 additions & 17 deletions app-modules/bot-discord/stubs/discord-php.stub
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ interface CollectionInterface

namespace Discord\Parts\User;

use Discord\Helpers\CollectionInterface;
use Discord\Parts\Guild\Role;

/**
* @property string $id
* @property string $username
Expand All @@ -42,6 +39,25 @@ class User
{
}

/**
* @property string $name
* @property int $type
* @property string|null $url
* @property string|null $application_id
* @property string|null $details
* @property string|null $state
*/
class Activity
{
}

namespace Discord\Parts\Guild\Member;

use Discord\Helpers\CollectionInterface;
use Discord\Parts\Guild\Role\Role;
use Discord\Parts\User\Activity;
use Discord\Parts\User\User;

/**
* @property string $id
* @property User|null $user
Expand All @@ -61,18 +77,6 @@ class Member
{
}

/**
* @property string $name
* @property int $type
* @property string|null $url
* @property string|null $application_id
* @property string|null $details
* @property string|null $state
*/
class Activity
{
}

namespace Discord\Parts\Guild;

/**
Expand All @@ -86,13 +90,15 @@ namespace Discord\Parts\Guild;
* @property int|null $member_count
* @property mixed $members
* @property mixed $channels
* @property \Discord\Helpers\CollectionInterface<Role> $roles
* @property \Discord\Helpers\CollectionInterface<\Discord\Parts\Guild\Role\Role> $roles
* @property-read mixed $voice_states
*/
class Guild
{
}

namespace Discord\Parts\Guild\Role;

/**
* @property string $id
* @property string $name
Expand Down Expand Up @@ -130,7 +136,7 @@ namespace Discord\Parts\WebSockets;
* @property \Discord\Parts\Channel\Channel|null $channel
* @property string $user_id
* @property \Discord\Parts\User\User|null $user
* @property \Discord\Parts\User\Member|null $member
* @property \Discord\Parts\Guild\Member\Member|null $member
* @property string $session_id
* @property bool $deaf
* @property bool $mute
Expand Down
2 changes: 1 addition & 1 deletion app-modules/docs/config/docs.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
declare(strict_types=1);

return [
'default_version' => '3.x',
'default_version' => '4.x',

'cache' => [
'enabled' => env('DOCS_CACHE_ENABLED', default: true),
Expand Down
2 changes: 1 addition & 1 deletion app-modules/docs/routes/docs-routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Route::get('docs', [DocsController::class, 'index'])->name('docs.index');

// The section is constrained to known document types so Scramble's
// `docs/3.x/api` (and any other prefix) falls through to its own route.
// `docs/4.x/api` (and any other prefix) falls through to its own route.
Route::get('docs/{section}/{path?}', [DocsController::class, 'show'])
->where('section', $sections)
->where('path', '.*')
Expand Down
6 changes: 3 additions & 3 deletions app-modules/docs/src/DocsServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ public function boot(): void
{
$this->commands([CacheDocsCommand::class]);

Scramble::registerApi('3.x');
Scramble::registerApi('4.x');

Scramble::registerUiRoute(path: 'docs/3.x/api', api: '3.x');
Scramble::registerJsonSpecificationRoute(path: 'docs/3.x/swagger.json', api: '3.x');
Scramble::registerUiRoute(path: 'docs/4.x/api', api: '4.x');
Scramble::registerJsonSpecificationRoute(path: 'docs/4.x/swagger.json', api: '4.x');
}
}
2 changes: 1 addition & 1 deletion app-modules/docs/src/Documentation.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static function replaceLinks($version, RenderedContentInterface|string $c
public static function getDocVersions(): array
{
return [
'3.x' => '3.x',
'4.x' => '4.x',
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@

it('does not let the catch-all hijack the Scramble api route', function (): void {
$route = resolve(Router::class)->getRoutes()->match(
Request::create('/docs/3.x/api', 'GET'),
Request::create('/docs/4.x/api', 'GET'),
);

expect($route->getActionName())->not->toContain(DocsController::class);
Expand Down
35 changes: 35 additions & 0 deletions app-modules/identity/routes/api-mobile-routes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

use He4rt\Identity\Auth\Http\Controllers\Mobile\MobileAuthController;
use He4rt\Identity\Auth\Http\Controllers\Mobile\MobileMeController;
use He4rt\Identity\Auth\Http\Controllers\Mobile\MobileOAuthController;
use Illuminate\Support\Facades\Route;

Route::prefix('api/mobile')
->middleware('api')
->group(static function (): void {
Route::prefix('auth')->group(static function (): void {
// O callback do OAuth é único por provider e já está cadastrado
// apontando pra rota web (auth/oauth/{provider} → OAuthController::
// getAuthenticate), que também finaliza o login mobile quando
// intent=MobileLogin. Ver He4rt\Identity\Auth\Support\MobileOAuthDeepLink.
Route::get('/{provider}/redirect', [MobileOAuthController::class, 'redirect'])
->name('mobile.oauth.redirect');

Route::post('/exchange', [MobileAuthController::class, 'exchange'])
->name('mobile.auth.exchange');

Route::post('/refresh', [MobileAuthController::class, 'refresh'])
->name('mobile.auth.refresh');

Route::post('/logout', [MobileAuthController::class, 'logout'])
->middleware('auth:api')
->name('mobile.auth.logout');
});

Route::get('/me', MobileMeController::class)
->middleware('auth:api')
->name('mobile.me');
});
44 changes: 44 additions & 0 deletions app-modules/identity/src/Auth/Actions/ExchangeMobileCodeAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace He4rt\Identity\Auth\Actions;

use He4rt\Identity\Auth\Exceptions\MobileAuthException;
use He4rt\Identity\User\Models\User;
use Illuminate\Support\Facades\Cache;

final readonly class ExchangeMobileCodeAction
{
public function execute(string $code): User
{
$cacheKey = IssueMobileExchangeCodeAction::cacheKey($code);

// Cache::pull() é get()+forget() como duas chamadas separadas — sob
// concorrência, dois requests podem ler o mesmo código antes de
// qualquer um apagar e mintar dois tokens da mesma autorização. O
// lock serializa get+forget num bloco atômico por código.
$lock = Cache::lock('identity:mobile-oauth-exchange-lock:'.$code, 10);

if (!$lock->get()) {
throw MobileAuthException::invalidExchangeCode();
}

try {
/** @var string|null $userId */
$userId = Cache::get($cacheKey);

throw_if($userId === null, MobileAuthException::invalidExchangeCode());

Cache::forget($cacheKey);

$user = User::query()->find($userId);

throw_if($user === null, MobileAuthException::invalidExchangeCode());

return $user;
} finally {
$lock->release();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public function execute(OAuthStateDTO $state, IdentityProvider $provider, string
$oauthUser = $client->getAuthenticatedUser($access);

$user = match ($state->intent) {
OAuthIntent::Login => $this->findOrCreateUser->execute($oauthUser),
OAuthIntent::Login, OAuthIntent::MobileLogin => $this->findOrCreateUser->execute($oauthUser),
OAuthIntent::Link => $this->resolveAuthenticatedUser(),
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace He4rt\Identity\Auth\Actions;

use He4rt\Identity\User\Models\User;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;

final readonly class IssueMobileExchangeCodeAction
{
private const int TTL_SECONDS = 60;

public static function cacheKey(string $code): string
{
return "identity:mobile-oauth-exchange:{$code}";
}

public function execute(User $user): string
{
$code = Str::random(40);

Cache::put(self::cacheKey($code), $user->id, self::TTL_SECONDS);

return $code;
}
}
28 changes: 28 additions & 0 deletions app-modules/identity/src/Auth/Actions/IssueMobileTokenAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace He4rt\Identity\Auth\Actions;

use He4rt\Identity\Auth\DTOs\MobileTokenDTO;
use He4rt\Identity\User\Models\User;
use Illuminate\Support\Facades\Auth;
use PHPOpenSourceSaver\JWTAuth\JWTGuard;

final readonly class IssueMobileTokenAction
{
public function execute(User $user): MobileTokenDTO
{
/** @var JWTGuard $guard */
$guard = Auth::guard('api');

/** @var string $token */
$token = $guard->login($user);

return new MobileTokenDTO(
accessToken: $token,
tokenType: 'bearer',
expiresIn: config()->integer('jwt.ttl') * 60,
);
}
}
Loading
Loading