Proje dosyaları eklendi
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Earn;
|
||||
|
||||
use App\Enums\EarnApiTokenStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EarnApiToken;
|
||||
use App\Models\EarnMember;
|
||||
use App\Services\EarnAuditLogger;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class EarnApiKeyController extends Controller
|
||||
{
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$member = $this->member($request);
|
||||
$tokens = $member->tokens()->latest()->get();
|
||||
|
||||
return response()->view('earn-panel.api-keys', [
|
||||
'tokens' => $tokens,
|
||||
'maxTotal' => (int) config('earn.api_keys.max_total'),
|
||||
'durations' => config('earn.api_keys.allowed_durations'),
|
||||
'defaultDuration' => config('earn.api_keys.default_duration'),
|
||||
'tasksUrl' => route('test-tasks.index'),
|
||||
])->withHeaders(['Cache-Control' => 'no-store, private', 'Pragma' => 'no-cache']);
|
||||
}
|
||||
|
||||
public function store(Request $request, EarnAuditLogger $audit): RedirectResponse
|
||||
{
|
||||
$request->merge(['name' => trim(strip_tags((string) $request->input('name')))]);
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'min:3', 'max:80'],
|
||||
'duration' => ['required', 'string', Rule::in(config('earn.api_keys.allowed_durations'))],
|
||||
]);
|
||||
$member = $this->member($request);
|
||||
$result = DB::transaction(function () use ($member, $data, $request, $audit): array {
|
||||
EarnMember::query()->whereKey($member->id)->lockForUpdate()->firstOrFail();
|
||||
if ($member->tokens()->count() >= (int) config('earn.api_keys.max_total')) {
|
||||
return ['error' => 'total'];
|
||||
}
|
||||
|
||||
$plainText = 'tpe_'.rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
|
||||
$expiresAt = $this->expiresAt($data['duration']);
|
||||
$token = $member->tokens()->create([
|
||||
'name' => $data['name'], 'token' => hash('sha256', $plainText), 'token_hint' => 'tpe_'.substr($plainText, -4),
|
||||
'abilities' => ['tasks:read'], 'is_active' => true, 'expires_at' => $expiresAt,
|
||||
]);
|
||||
$audit->record('api_key_created', $member, $request, $this->metadata($token));
|
||||
|
||||
return ['token' => $plainText];
|
||||
}, 3);
|
||||
|
||||
if (isset($result['error'])) {
|
||||
$audit->record('api_key_limit_rejected', $member, $request, ['device_name' => $data['name'], 'abilities' => ['tasks:read'], 'limit' => $result['error']]);
|
||||
throw ValidationException::withMessages(['api_key' => __('earn_portal.total_limit_reached')]);
|
||||
}
|
||||
|
||||
return redirect()->route('earn.api-keys.index')->with('new_api_key', $result['token']);
|
||||
}
|
||||
|
||||
public function status(Request $request, int $apiKey, EarnAuditLogger $audit): RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['is_active' => ['required', 'boolean']]);
|
||||
$member = $this->member($request);
|
||||
DB::transaction(function () use ($member, $apiKey, $data, $request, $audit): void {
|
||||
EarnMember::query()->whereKey($member->id)->lockForUpdate()->firstOrFail();
|
||||
$token = $member->tokens()->whereKey($apiKey)->lockForUpdate()->firstOrFail();
|
||||
abort_if(in_array($token->status(), [EarnApiTokenStatus::Expired, EarnApiTokenStatus::Revoked], true), 422);
|
||||
$activate = (bool) $data['is_active'];
|
||||
if ($token->is_active === $activate) {
|
||||
return;
|
||||
}
|
||||
$token->forceFill(['is_active' => $activate])->save();
|
||||
$audit->record($activate ? 'api_key_activated' : 'api_key_deactivated', $member, $request, $this->metadata($token));
|
||||
}, 3);
|
||||
|
||||
return back()->with('status', __('earn_portal.status_updated'));
|
||||
}
|
||||
|
||||
public function revoke(Request $request, int $apiKey, EarnAuditLogger $audit): RedirectResponse
|
||||
{
|
||||
$member = $this->member($request);
|
||||
DB::transaction(function () use ($member, $apiKey, $request, $audit): void {
|
||||
$token = $member->tokens()->whereKey($apiKey)->lockForUpdate()->firstOrFail();
|
||||
abort_if($token->revoked_at !== null, 404);
|
||||
$token->forceFill(['revoked_at' => now(), 'is_active' => false])->save();
|
||||
$audit->record('api_key_revoked', $member, $request, $this->metadata($token));
|
||||
}, 3);
|
||||
|
||||
return back()->with('status', __('earn_portal.key_revoked'));
|
||||
}
|
||||
|
||||
public function destroy(Request $request, int $apiKey, EarnAuditLogger $audit): RedirectResponse
|
||||
{
|
||||
$member = $this->member($request);
|
||||
DB::transaction(function () use ($member, $apiKey, $request, $audit): void {
|
||||
$token = $member->tokens()->whereKey($apiKey)->lockForUpdate()->firstOrFail();
|
||||
$token->forceFill(['revoked_at' => $token->revoked_at ?? now(), 'is_active' => false])->save();
|
||||
$metadata = $this->metadata($token) + ['deleted_at' => now()->toIso8601String(), 'actor_earn_member_id' => $member->id];
|
||||
$audit->record('api_key_deleted', $member, $request, $metadata);
|
||||
$token->delete();
|
||||
}, 3);
|
||||
|
||||
return back()->with('status', __('earn_portal.key_deleted'));
|
||||
}
|
||||
|
||||
private function expiresAt(string $duration): ?Carbon
|
||||
{
|
||||
return match ($duration) {
|
||||
'1_day' => now()->addDay(), '7_days' => now()->addDays(7), '30_days' => now()->addDays(30),
|
||||
'90_days' => now()->addDays(90), '180_days' => now()->addDays(180), '1_year' => now()->addYear(),
|
||||
'unlimited' => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function metadata(EarnApiToken $token): array
|
||||
{
|
||||
return ['token_id' => $token->id, 'device_name' => $token->name, 'abilities' => $token->abilities,
|
||||
'created_at' => $token->created_at?->toIso8601String(), 'expires_at' => $token->expires_at?->toIso8601String(),
|
||||
'revoked_at' => $token->revoked_at?->toIso8601String()];
|
||||
}
|
||||
|
||||
private function member(Request $request): EarnMember
|
||||
{
|
||||
$member = $request->user('earn');
|
||||
abort_unless($member instanceof EarnMember, 403);
|
||||
|
||||
return $member;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Earn;
|
||||
|
||||
use App\Enums\EarnLegalDocumentType;
|
||||
use App\Enums\EarnMemberStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\EarnMember;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EarnAuthController extends Controller
|
||||
{
|
||||
public function loginForm(): View
|
||||
{
|
||||
return view('earn-auth.login');
|
||||
}
|
||||
|
||||
public function registerForm(): View
|
||||
{
|
||||
return view('earn-auth.register');
|
||||
}
|
||||
|
||||
public function register(Request $request): RedirectResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'email' => ['required', 'email', 'max:255', 'unique:earn_members,email'],
|
||||
'password' => ['required', 'confirmed', Password::defaults()],
|
||||
'terms_accepted' => ['accepted'],
|
||||
'privacy_notice_acknowledged' => ['accepted'],
|
||||
], [
|
||||
'terms_accepted.accepted' => __('earn_legal.validation.terms_accepted'),
|
||||
'privacy_notice_acknowledged.accepted' => __('earn_legal.validation.privacy_notice_acknowledged'),
|
||||
]);
|
||||
|
||||
$member = DB::transaction(function () use ($data, $request): EarnMember {
|
||||
$member = EarnMember::query()->create([
|
||||
'name' => trim($data['name']),
|
||||
'email' => Str::lower($data['email']),
|
||||
'password' => $data['password'],
|
||||
'status' => EarnMemberStatus::Pending,
|
||||
]);
|
||||
|
||||
$context = ['accepted_at' => now(), 'ip_address' => $request->ip(), 'user_agent' => Str::limit((string) $request->userAgent(), 1000, '')];
|
||||
$member->legalAcceptances()->create($context + ['document_type' => EarnLegalDocumentType::Terms, 'document_version' => config('earn.legal.terms.version')]);
|
||||
$member->legalAcceptances()->create($context + ['document_type' => EarnLegalDocumentType::PrivacyNotice, 'document_version' => config('earn.legal.privacy.version')]);
|
||||
|
||||
return $member;
|
||||
});
|
||||
|
||||
event(new Registered($member));
|
||||
Auth::guard('earn')->login($member);
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect()->route('earn.verification.notice');
|
||||
}
|
||||
|
||||
public function login(Request $request): RedirectResponse
|
||||
{
|
||||
$credentials = $request->validate([
|
||||
'email' => ['required', 'email'],
|
||||
'password' => ['required', 'string'],
|
||||
]);
|
||||
$key = 'earn-login:'.Str::transliterate(Str::lower($credentials['email']).'|'.$request->ip());
|
||||
|
||||
if (RateLimiter::tooManyAttempts($key, 5)) {
|
||||
throw ValidationException::withMessages(['email' => __('ui.errors.throttle')]);
|
||||
}
|
||||
|
||||
if (! Auth::guard('earn')->attempt($credentials, $request->boolean('remember'))) {
|
||||
RateLimiter::hit($key, 60);
|
||||
throw ValidationException::withMessages(['email' => __('ui.errors.credentials')]);
|
||||
}
|
||||
|
||||
$member = Auth::guard('earn')->user();
|
||||
if (! $member->canUseApi()) {
|
||||
Auth::guard('earn')->logout();
|
||||
RateLimiter::hit($key, 60);
|
||||
throw ValidationException::withMessages(['email' => __('earn_portal.account_unavailable')]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($key);
|
||||
$request->session()->regenerate();
|
||||
$member->forceFill(['last_login_at' => now(), 'last_login_ip' => $request->ip()])->saveQuietly();
|
||||
|
||||
return redirect()->intended(route('earn.dashboard'));
|
||||
}
|
||||
|
||||
public function logout(Request $request): RedirectResponse
|
||||
{
|
||||
Auth::guard('earn')->logout();
|
||||
$request->session()->regenerate();
|
||||
$request->session()->regenerateToken();
|
||||
|
||||
return redirect()->route('earn.login');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Earn;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EarnDashboardController extends Controller
|
||||
{
|
||||
public function __invoke(): View
|
||||
{
|
||||
$member = auth('earn')->user();
|
||||
$activeTokenCount = $member->tokens()
|
||||
->where(fn ($query) => $query->whereNull('expires_at')->orWhere('expires_at', '>', now()))
|
||||
->count();
|
||||
|
||||
return view('earn-panel.dashboard', compact('member', 'activeTokenCount'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Earn;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EarnLegalPageController extends Controller
|
||||
{
|
||||
public function terms(): View
|
||||
{
|
||||
return $this->page('terms');
|
||||
}
|
||||
|
||||
public function privacy(): View
|
||||
{
|
||||
return $this->page('privacy');
|
||||
}
|
||||
|
||||
private function page(string $document): View
|
||||
{
|
||||
return view('earn-legal.document', [
|
||||
'document' => $document,
|
||||
'content' => __('earn_legal.'.$document),
|
||||
'version' => config('earn.legal.'.$document.'.version'),
|
||||
'effectiveDate' => config('earn.legal.'.$document.'.effective_date'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Earn;
|
||||
|
||||
use App\Enums\EarnMemberStatus;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class EarnVerificationController extends Controller
|
||||
{
|
||||
public function notice(): View|RedirectResponse
|
||||
{
|
||||
if (auth('earn')->user()->hasVerifiedEmail()) {
|
||||
return redirect()->route('earn.dashboard');
|
||||
}
|
||||
|
||||
return view('earn-auth.verify');
|
||||
}
|
||||
|
||||
public function send(): RedirectResponse
|
||||
{
|
||||
if (! auth('earn')->user()->hasVerifiedEmail()) {
|
||||
auth('earn')->user()->sendEmailVerificationNotification();
|
||||
}
|
||||
|
||||
return back()->with('status', __('earn_portal.verification_sent'));
|
||||
}
|
||||
|
||||
public function verify(Request $request): RedirectResponse
|
||||
{
|
||||
$member = auth('earn')->user();
|
||||
abort_unless((string) $member->getKey() === (string) $request->route('id'), 403);
|
||||
abort_unless(hash_equals((string) $request->route('hash'), sha1($member->getEmailForVerification())), 403);
|
||||
|
||||
if (! $member->hasVerifiedEmail()) {
|
||||
$member->forceFill(['email_verified_at' => now(), 'status' => EarnMemberStatus::Active])->save();
|
||||
}
|
||||
|
||||
return redirect()->route('earn.dashboard');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user