Skip to content

Commit

Permalink
Merge pull request #129 from wri/release/desert-date
Browse files Browse the repository at this point in the history
[RELEASE] Dessert Date
  • Loading branch information
roguenet authored Apr 4, 2024
2 parents 840213c + 1482370 commit cad0207
Show file tree
Hide file tree
Showing 41 changed files with 745 additions and 119 deletions.
76 changes: 76 additions & 0 deletions app/Auth/ServiceAccountGuard.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace App\Auth;

use App\Models\User;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Http\Request;
use Tymon\JWTAuth\Exceptions\TokenInvalidException;
use Tymon\JWTAuth\Validators\TokenValidator;

class ServiceAccountGuard implements Guard
{
use GuardHelpers;

const HEADER = 'authorization';
CONST PREFIX = 'bearer';
const API_KEY_LENGTH = 64;

protected Request $request;

public function __construct(Request $request)
{
$this->request = $request;
}

public function user(): ?Authenticatable
{
if ($this->user !== null) {
return $this->user;
}

$apiKey = $this->getApiKey();
if ($apiKey == null) {
return null;
}

return $this->user = User::where('api_key', $apiKey)->first();
}

public function validate(array $credentials = []): bool
{
// There's no logging in or validating for this guard.
return false;
}

protected function getApiKey(): ?string
{
$header = $this->request->headers->get(self::HEADER);
if ($header == null) {
return null;
}

$position = strripos($header, self::PREFIX);
if ($position === false) {
return null;
}

$bearerValue = trim(substr($header, $position + strlen(self::PREFIX)));
if (strlen($bearerValue) !== self::API_KEY_LENGTH || $this->isJwt($bearerValue)) {
return null;
}

return $bearerValue;
}

protected function isJwt($value): bool
{
try {
return (new TokenValidator())->check($value) != null;
} catch (TokenInvalidException $exception) {
return false;
}
}
}
62 changes: 62 additions & 0 deletions app/Console/Commands/CreateServiceAccount.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

namespace App\Console\Commands;

use App\Models\V2\User;
use App\Validators\ServiceAccountValidator;
use DateTime;
use DateTimeZone;
use Exception;
use Illuminate\Console\Command;
use Spatie\Permission\Models\Role;

class CreateServiceAccount extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'create-service-account {email}';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Creates a service account with the given email address.';

/**
* Execute the console command.
*/
public function handle()
{
try {
$email = $this->argument('email');

$apiKey = base64_encode(random_bytes(48));

$data = [
'email_address' => $email,
'api_key' => $apiKey,
];
ServiceAccountValidator::validate('CREATE', $data);

$data['role'] = 'service';
$data['email_address_verified_at'] = new DateTime('now', new DateTimeZone('UTC'));

$user = new User($data);
$user->saveOrFail();
// TODO Allow other types of service account, when/if necessary.
$user->assignRole('greenhouse-service-account');

$this->info("Created service account $email with API Key: $apiKey");
return 0;

} catch (Exception $exception) {
$this->error($exception->getMessage());
$this->error('Creation failed');
return -1;
}
}
}
46 changes: 46 additions & 0 deletions app/Console/Commands/OneOff/CreateGreenhouseServiceAccountRole.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Console\Commands\OneOff;

use Illuminate\Console\Command;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;

class CreateGreenhouseServiceAccountRole extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'one-off:create-greenhouse-service-account-role';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Creates the Greenhouse service account role';

/**
* Execute the console command.
*/
public function handle()
{
// Keep this command idempotent
$role = Role::where('name', 'greenhouse-service-account')->first();
if ($role == null) {
$role = Role::create(['name' => 'greenhouse-service-account']);
}

// Make sure all permissions in config/permissions have been created.
$permissionKeys = array_keys(config('wri.permissions'));
foreach ($permissionKeys as $key) {
if (Permission::where('name', $key)->count() === 0) {
Permission::create(['name' => $key]);
}
}

$role->syncPermissions(['projects-read', 'polygons-manage', 'media-manage']);
}
}
6 changes: 3 additions & 3 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Helpers\ErrorHelper;
use App\Helpers\JsonResponseHelper;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
Expand Down Expand Up @@ -161,9 +162,7 @@ public function report(Throwable $exception)
return;
default:
if (config('app.env') != 'local') {
if (config('app.env') == 'production') {
App::make('sentry')->captureException($exception);
}
App::make('sentry')->captureException($exception);
Log::error($exception);
}

Expand All @@ -174,6 +173,7 @@ public function report(Throwable $exception)
public function render($request, Throwable $exception)
{
switch (get_class($exception)) {
case AuthenticationException::class:
case AuthorizationException::class:
return JsonResponseHelper::error([], 403);
case FailedLoginException::class:
Expand Down
4 changes: 4 additions & 0 deletions app/Exports/V2/ApplicationExport.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ class ApplicationExport extends BaseExportFormSubmission implements WithHeadings
'Headquarters address State/Province',
'Headquarters address Zipcode',
'Proof of local legal registration, incorporation, or right to operate',
'Created At',
'Updated At',
'Website URL (optional)',
'Organization Facebook URL(optional)',
'Organization Instagram URL(optional)',
Expand Down Expand Up @@ -120,6 +122,8 @@ public function map($application): array
$organisation->hq_state,
$organisation->hq_zipcode,
$this->handleOrganisationFiles($organisation, 'legal_registration'),
$application->created_at,
$application->updated_at,
$organisation->web_url,
$organisation->facebook_url,
$organisation->instagram_url,
Expand Down
2 changes: 1 addition & 1 deletion app/Exports/V2/BaseExportFormSubmission.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ protected function getAnswer(array $field, array $answers): string
return $this->stringifyModel($answer, ['first_name', 'last_name', 'position', 'gender', 'age', 'role']);

case 'fundingType':
return $this->stringifyModel($answer, ['source', 'amount', 'year']);
return $this->stringifyModel($answer, ['type', 'source', 'amount', 'year']);

case 'ownershipStake':
return $this->stringifyModel($answer, ['first_name', 'last_name', 'title', 'gender', 'percent_ownership', 'year_of_birth',]);
Expand Down
5 changes: 2 additions & 3 deletions app/Helpers/I18nHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,7 @@ public static function generateI18nItem(Model $target, string $property): ?int
$shouldGenerateI18nItem = I18nHelper::shouldGenerateI18nItem($target, $property);
$value = trim(data_get($target, $property, false));
if ($shouldGenerateI18nItem) {
$short = strlen($value) <= 256;

$short = strlen($value) < 256;
$i18nItem = I18nItem::create([
'type' => $short ? 'short' : 'long',
'status' => I18nItem::STATUS_DRAFT,
Expand Down Expand Up @@ -47,7 +46,7 @@ public static function shouldGenerateI18nItem(Model $target, string $property):

$oldType = $i18nItem->type;

$short = strlen($value) <= 256;
$short = strlen($value) < 256;
$type = $short ? 'short' : 'long';

if ($oldType !== $type) {
Expand Down
1 change: 0 additions & 1 deletion app/Http/Controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use App\Http\Requests\ResendRequest;
use App\Http\Requests\ResetRequest;
use App\Http\Requests\VerifyRequest;
use App\Http\Resources\V2\Projects\ProjectInviteResource;
use App\Http\Resources\V2\User\MeResource;
use App\Jobs\ResetPasswordJob;
use App\Jobs\UserVerificationJob;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,7 @@ class EntityUpdateRequestsController extends Controller
public function __invoke(Request $request, EntityModel $entity)
{
$this->authorize('read', $entity);
$latest = $entity->updateRequests()
->orderBy('updated_at', 'DESC')
->first();
$latest = $entity->updateRequests()->isUnapproved()->orderBy('updated_at', 'DESC')->first();

if (is_null($latest)) {
return new JsonResponse('There is not any update request for this resource', 404);
Expand Down
5 changes: 3 additions & 2 deletions app/Http/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ class Kernel extends HttpKernel
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'auth:service-api-key,api',
'bindings',
],
];
Expand All @@ -50,7 +51,7 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
Expand All @@ -75,7 +76,7 @@ class Kernel extends HttpKernel
protected $middlewarePriority = [
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Auth\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
Expand Down
17 changes: 0 additions & 17 deletions app/Http/Middleware/Authenticate.php

This file was deleted.

53 changes: 53 additions & 0 deletions app/Jobs/V2/SendEntityStatusChangeEmailsJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

namespace App\Jobs\V2;

use App\Mail\EntityStatusChange as EntityStatusChangeMail;
use App\Models\V2\EntityModel;
use App\StateMachines\EntityStatusStateMachine;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class SendEntityStatusChangeEmailsJob implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
use Queueable;
use SerializesModels;

private EntityModel $entity;

public function __construct(EntityModel $entity)
{
$this->entity = $entity;
}

public function handle(): void
{
if ($this->entity->status != EntityStatusStateMachine::APPROVED &&
$this->entity->status != EntityStatusStateMachine::NEEDS_MORE_INFORMATION &&
$this->entity->update_request_status != EntityStatusStateMachine::NEEDS_MORE_INFORMATION) {
return;
}

$emailAddresses = $this->entity->project->users()->pluck('email_address');
if (empty($emailAddresses)) {
return;
}

// TODO: This is a temporary hack to avoid spamming folks that have a funky role right now. In the future,
// they will have a different role, and we can simply skip sending this email to anybody with that role.
$skipRecipients = collect(explode(',', getenv('ENTITY_UPDATE_DO_NOT_EMAIL')));
foreach ($emailAddresses as $emailAddress) {
if ($skipRecipients->contains($emailAddress)) {
continue;
}

Mail::to($emailAddress)->send(new EntityStatusChangeMail($this->entity));
}
}
}
Loading

0 comments on commit cad0207

Please sign in to comment.