mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-10 08:52:39 +01:00
Add credential rotation and statuses (disabled, unsubscribed) to Phortune external email
Summary: Depends on D20737. Ref T13367. Allow external addresses to have their access key rotated. Account managers can disable them, and anyone with the link can permanently unsubscribe them. Test Plan: Enabled/disabled addresses; permanently unsubscribed addresses. Maniphest Tasks: T13367 Differential Revision: https://secure.phabricator.com/D20738
This commit is contained in:
parent
8f6a1ab015
commit
4e13551e85
11 changed files with 434 additions and 4 deletions
|
@ -5237,7 +5237,11 @@ phutil_register_library_map(array(
|
|||
'PhortuneAccountEmailEditor' => 'applications/phortune/editor/PhortuneAccountEmailEditor.php',
|
||||
'PhortuneAccountEmailPHIDType' => 'applications/phortune/phid/PhortuneAccountEmailPHIDType.php',
|
||||
'PhortuneAccountEmailQuery' => 'applications/phortune/query/PhortuneAccountEmailQuery.php',
|
||||
'PhortuneAccountEmailRotateController' => 'applications/phortune/controller/account/PhortuneAccountEmailRotateController.php',
|
||||
'PhortuneAccountEmailRotateTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailRotateTransaction.php',
|
||||
'PhortuneAccountEmailStatus' => 'applications/phortune/constants/PhortuneAccountEmailStatus.php',
|
||||
'PhortuneAccountEmailStatusController' => 'applications/phortune/controller/account/PhortuneAccountEmailStatusController.php',
|
||||
'PhortuneAccountEmailStatusTransaction' => 'applications/phortune/xaction/PhortuneAccountEmailStatusTransaction.php',
|
||||
'PhortuneAccountEmailTransaction' => 'applications/phortune/storage/PhortuneAccountEmailTransaction.php',
|
||||
'PhortuneAccountEmailTransactionQuery' => 'applications/phortune/query/PhortuneAccountEmailTransactionQuery.php',
|
||||
'PhortuneAccountEmailTransactionType' => 'applications/phortune/xaction/PhortuneAccountEmailTransactionType.php',
|
||||
|
@ -5296,6 +5300,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php',
|
||||
'PhortuneExternalController' => 'applications/phortune/controller/external/PhortuneExternalController.php',
|
||||
'PhortuneExternalOverviewController' => 'applications/phortune/controller/external/PhortuneExternalOverviewController.php',
|
||||
'PhortuneExternalUnsubscribeController' => 'applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php',
|
||||
'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php',
|
||||
'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php',
|
||||
'PhortuneMemberHasAccountEdgeType' => 'applications/phortune/edge/PhortuneMemberHasAccountEdgeType.php',
|
||||
|
@ -11815,7 +11820,11 @@ phutil_register_library_map(array(
|
|||
'PhortuneAccountEmailEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
'PhortuneAccountEmailPHIDType' => 'PhabricatorPHIDType',
|
||||
'PhortuneAccountEmailQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhortuneAccountEmailRotateController' => 'PhortuneAccountController',
|
||||
'PhortuneAccountEmailRotateTransaction' => 'PhortuneAccountEmailTransactionType',
|
||||
'PhortuneAccountEmailStatus' => 'Phobject',
|
||||
'PhortuneAccountEmailStatusController' => 'PhortuneAccountController',
|
||||
'PhortuneAccountEmailStatusTransaction' => 'PhortuneAccountEmailTransactionType',
|
||||
'PhortuneAccountEmailTransaction' => 'PhabricatorModularTransaction',
|
||||
'PhortuneAccountEmailTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhortuneAccountEmailTransactionType' => 'PhabricatorModularTransactionType',
|
||||
|
@ -11883,6 +11892,7 @@ phutil_register_library_map(array(
|
|||
'PhortuneErrCode' => 'PhortuneConstants',
|
||||
'PhortuneExternalController' => 'PhortuneController',
|
||||
'PhortuneExternalOverviewController' => 'PhortuneExternalController',
|
||||
'PhortuneExternalUnsubscribeController' => 'PhortuneExternalController',
|
||||
'PhortuneInvoiceView' => 'AphrontTagView',
|
||||
'PhortuneLandingController' => 'PhortuneController',
|
||||
'PhortuneMemberHasAccountEdgeType' => 'PhabricatorEdgeType',
|
||||
|
|
|
@ -87,7 +87,12 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
|
|||
),
|
||||
'addresses/' => array(
|
||||
'' => 'PhortuneAccountEmailAddressesController',
|
||||
'(?P<id>\d+)/' => 'PhortuneAccountEmailViewController',
|
||||
'(?P<addressID>\d+)/' => array(
|
||||
'' => 'PhortuneAccountEmailViewController',
|
||||
'rotate/' => 'PhortuneAccountEmailRotateController',
|
||||
'(?P<action>disable|enable)/'
|
||||
=> 'PhortuneAccountEmailStatusController',
|
||||
),
|
||||
$this->getEditRoutePattern('edit/')
|
||||
=> 'PhortuneAccountEmailEditController',
|
||||
),
|
||||
|
@ -106,6 +111,7 @@ final class PhabricatorPhortuneApplication extends PhabricatorApplication {
|
|||
),
|
||||
'external/(?P<addressKey>[^/]+)/(?P<accessKey>[^/]+)/' => array(
|
||||
'' => 'PhortuneExternalOverviewController',
|
||||
'unsubscribe/' => 'PhortuneExternalUnsubscribeController',
|
||||
),
|
||||
'merchant/' => array(
|
||||
$this->getQueryRoutePattern()
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneAccountEmailRotateController
|
||||
extends PhortuneAccountController {
|
||||
|
||||
protected function shouldRequireAccountEditCapability() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function handleAccountRequest(AphrontRequest $request) {
|
||||
$viewer = $this->getViewer();
|
||||
$account = $this->getAccount();
|
||||
|
||||
$address = id(new PhortuneAccountEmailQuery())
|
||||
->setViewer($viewer)
|
||||
->withAccountPHIDs(array($account->getPHID()))
|
||||
->withIDs(array($request->getURIData('addressID')))
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->executeOne();
|
||||
if (!$address) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$address_uri = $address->getURI();
|
||||
|
||||
if ($request->isFormOrHisecPost()) {
|
||||
$xactions = array();
|
||||
|
||||
$xactions[] = $address->getApplicationTransactionTemplate()
|
||||
->setTransactionType(
|
||||
PhortuneAccountEmailRotateTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue(true);
|
||||
|
||||
$address->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setCancelURI($address_uri)
|
||||
->applyTransactions($address, $xactions);
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($address_uri);
|
||||
}
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Rotate Access Key'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'Rotate the access key for email address %s?',
|
||||
phutil_tag('strong', array(), $address->getAddress())))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'Existing access links which have been sent to this email address '.
|
||||
'will stop working.'))
|
||||
->addSubmitButton(pht('Rotate Access Key'))
|
||||
->addCancelButton($address_uri);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneAccountEmailStatusController
|
||||
extends PhortuneAccountController {
|
||||
|
||||
protected function shouldRequireAccountEditCapability() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function handleAccountRequest(AphrontRequest $request) {
|
||||
$viewer = $this->getViewer();
|
||||
$account = $this->getAccount();
|
||||
|
||||
$address = id(new PhortuneAccountEmailQuery())
|
||||
->setViewer($viewer)
|
||||
->withAccountPHIDs(array($account->getPHID()))
|
||||
->withIDs(array($request->getURIData('addressID')))
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->executeOne();
|
||||
if (!$address) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$address_uri = $address->getURI();
|
||||
|
||||
$is_enable = false;
|
||||
$is_disable = false;
|
||||
|
||||
$old_status = $address->getStatus();
|
||||
switch ($request->getURIData('action')) {
|
||||
case 'enable':
|
||||
if ($old_status === PhortuneAccountEmailStatus::STATUS_ACTIVE) {
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Already Enabled'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'You can not enable this address because it is already '.
|
||||
'active.'))
|
||||
->addCancelButton($address_uri);
|
||||
}
|
||||
|
||||
if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) {
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Permanently Unsubscribed'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'You can not enable this address because it has been '.
|
||||
'permanently unsubscribed.'))
|
||||
->addCancelButton($address_uri);
|
||||
}
|
||||
|
||||
$new_status = PhortuneAccountEmailStatus::STATUS_ACTIVE;
|
||||
$is_enable = true;
|
||||
break;
|
||||
case 'disable':
|
||||
if ($old_status === PhortuneAccountEmailStatus::STATUS_DISABLED) {
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Already Disabled'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'You can not disabled this address because it is already '.
|
||||
'disabled.'))
|
||||
->addCancelButton($address_uri);
|
||||
}
|
||||
|
||||
if ($old_status === PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED) {
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Permanently Unsubscribed'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'You can not disable this address because it has been '.
|
||||
'permanently unsubscribed.'))
|
||||
->addCancelButton($address_uri);
|
||||
}
|
||||
|
||||
$new_status = PhortuneAccountEmailStatus::STATUS_DISABLED;
|
||||
$is_disable = true;
|
||||
break;
|
||||
default:
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
if ($request->isFormOrHisecPost()) {
|
||||
$xactions = array();
|
||||
|
||||
$xactions[] = $address->getApplicationTransactionTemplate()
|
||||
->setTransactionType(
|
||||
PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue($new_status);
|
||||
|
||||
$address->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setCancelURI($address_uri)
|
||||
->applyTransactions($address, $xactions);
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($address_uri);
|
||||
}
|
||||
|
||||
$dialog = $this->newDialog();
|
||||
|
||||
$body = array();
|
||||
|
||||
if ($is_disable) {
|
||||
$title = pht('Disable Address');
|
||||
|
||||
$body[] = pht(
|
||||
'This address will no longer receive email, and access links will '.
|
||||
'no longer function.');
|
||||
|
||||
$submit = pht('Disable Address');
|
||||
} else {
|
||||
$title = pht('Enable Address');
|
||||
|
||||
$body[] = pht(
|
||||
'This address will receive email again, and existing links '.
|
||||
'to access order history will work again.');
|
||||
|
||||
$submit = pht('Enable Address');
|
||||
}
|
||||
|
||||
foreach ($body as $graph) {
|
||||
$dialog->appendParagraph($graph);
|
||||
}
|
||||
|
||||
return $dialog
|
||||
->setTitle($title)
|
||||
->addCancelButton($address_uri)
|
||||
->addSubmitButton($submit);
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ final class PhortuneAccountEmailViewController
|
|||
$address = id(new PhortuneAccountEmailQuery())
|
||||
->setViewer($viewer)
|
||||
->withAccountPHIDs(array($account->getPHID()))
|
||||
->withIDs(array($request->getURIData('id')))
|
||||
->withIDs(array($request->getURIData('addressID')))
|
||||
->executeOne();
|
||||
if (!$address) {
|
||||
return new Aphront404Response();
|
||||
|
@ -83,6 +83,56 @@ final class PhortuneAccountEmailViewController
|
|||
->setDisabled(!$can_edit)
|
||||
->setWorkflow(!$can_edit));
|
||||
|
||||
switch ($address->getStatus()) {
|
||||
case PhortuneAccountEmailStatus::STATUS_ACTIVE:
|
||||
$disable_name = pht('Disable Address');
|
||||
$disable_icon = 'fa-times';
|
||||
$can_disable = true;
|
||||
$disable_action = 'disable';
|
||||
break;
|
||||
case PhortuneAccountEmailStatus::STATUS_DISABLED:
|
||||
$disable_name = pht('Enable Address');
|
||||
$disable_icon = 'fa-check';
|
||||
$can_disable = true;
|
||||
$disable_action = 'enable';
|
||||
break;
|
||||
case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED:
|
||||
$disable_name = pht('Disable Address');
|
||||
$disable_icon = 'fa-times';
|
||||
$can_disable = false;
|
||||
$disable_action = 'disable';
|
||||
break;
|
||||
}
|
||||
|
||||
$disable_uri = $this->getApplicationURI(
|
||||
urisprintf(
|
||||
'account/%d/addresses/%d/%s/',
|
||||
$account->getID(),
|
||||
$address->getID(),
|
||||
$disable_action));
|
||||
|
||||
$curtain->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName($disable_name)
|
||||
->setIcon($disable_icon)
|
||||
->setHref($disable_uri)
|
||||
->setDisabled(!$can_disable)
|
||||
->setWorkflow(true));
|
||||
|
||||
$rotate_uri = $this->getApplicationURI(
|
||||
urisprintf(
|
||||
'account/%d/addresses/%d/rotate/',
|
||||
$account->getID(),
|
||||
$address->getID()));
|
||||
|
||||
$curtain->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName(pht('Rotate Access Key'))
|
||||
->setIcon('fa-refresh')
|
||||
->setHref($rotate_uri)
|
||||
->setDisabled(!$can_edit)
|
||||
->setWorkflow(true));
|
||||
|
||||
$curtain->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName(pht('Show External View'))
|
||||
|
@ -100,7 +150,23 @@ final class PhortuneAccountEmailViewController
|
|||
$view = id(new PHUIPropertyListView())
|
||||
->setUser($viewer);
|
||||
|
||||
$access_key = $address->getAccessKey();
|
||||
|
||||
// This is not a meaningful security barrier: the full plaintext of the
|
||||
// access key is visible on the page in the link target of the "Show
|
||||
// External View" action. It's just here to make it clear "Rotate Access
|
||||
// Key" actually does something.
|
||||
|
||||
$prefix_length = 4;
|
||||
$visible_part = substr($access_key, 0, $prefix_length);
|
||||
$masked_part = str_repeat(
|
||||
"\xE2\x80\xA2",
|
||||
strlen($access_key) - $prefix_length);
|
||||
$access_display = $visible_part.$masked_part;
|
||||
$access_display = phutil_tag('tt', array(), $access_display);
|
||||
|
||||
$view->addProperty(pht('Email Address'), $address->getAddress());
|
||||
$view->addProperty(pht('Access Key'), $access_display);
|
||||
|
||||
return id(new PHUIObjectBoxView())
|
||||
->setHeaderText(pht('Email Address Details'))
|
||||
|
|
|
@ -83,7 +83,29 @@ abstract class PhortuneExternalController
|
|||
return $dialog;
|
||||
}
|
||||
|
||||
// TODO: Test that status is good.
|
||||
switch ($email->getStatus()) {
|
||||
case PhortuneAccountEmailStatus::STATUS_ACTIVE:
|
||||
break;
|
||||
case PhortuneAccountEmailStatus::STATUS_DISABLED:
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Address Disabled'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'This email address (%s) has been disabled and no longer has '.
|
||||
'access to this payment account.',
|
||||
$email_display));
|
||||
case PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED:
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Permanently Unsubscribed'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'This email address (%s) has been permanently unsubscribed '.
|
||||
'and no longer has access to this payment account.',
|
||||
$email_display));
|
||||
break;
|
||||
default:
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$this->email = $email;
|
||||
|
||||
|
|
|
@ -12,7 +12,14 @@ final class PhortuneExternalOverviewController
|
|||
->setBorder(true);
|
||||
|
||||
$header = id(new PHUIHeaderView())
|
||||
->setHeader(pht('Invoices and Receipts: %s', $account->getName()));
|
||||
->setHeader(pht('Invoices and Receipts: %s', $account->getName()))
|
||||
->addActionLink(
|
||||
id(new PHUIButtonView())
|
||||
->setTag('a')
|
||||
->setIcon('fa-times')
|
||||
->setText(pht('Unsubscribe'))
|
||||
->setHref($email->getUnsubscribeURI())
|
||||
->setWorkflow(true));
|
||||
|
||||
$external_view = $this->newExternalView();
|
||||
$invoices_view = $this->newInvoicesView();
|
||||
|
|
67
src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php
vendored
Normal file
67
src/applications/phortune/controller/external/PhortuneExternalUnsubscribeController.php
vendored
Normal file
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneExternalUnsubscribeController
|
||||
extends PhortuneExternalController {
|
||||
|
||||
protected function handleExternalRequest(AphrontRequest $request) {
|
||||
$xviewer = $this->getExternalViewer();
|
||||
$email = $this->getAccountEmail();
|
||||
$account = $email->getAccount();
|
||||
|
||||
$email_uri = $email->getExternalURI();
|
||||
|
||||
if ($request->isFormOrHisecPost()) {
|
||||
$xactions = array();
|
||||
|
||||
$xactions[] = $email->getApplicationTransactionTemplate()
|
||||
->setTransactionType(
|
||||
PhortuneAccountEmailStatusTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue(PhortuneAccountEmailStatus::STATUS_UNSUBSCRIBED);
|
||||
|
||||
$email->getApplicationTransactionEditor()
|
||||
->setActor($xviewer)
|
||||
->setActingAsPHID($email->getPHID())
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setCancelURI($email_uri)
|
||||
->applyTransactions($email, $xactions);
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($email_uri);
|
||||
}
|
||||
|
||||
$email_display = phutil_tag(
|
||||
'strong',
|
||||
array(),
|
||||
$email->getAddress());
|
||||
|
||||
$account_display = phutil_tag(
|
||||
'strong',
|
||||
array(),
|
||||
$account->getName());
|
||||
|
||||
$submit = pht(
|
||||
'Permanently Unsubscribe (%s)',
|
||||
$email->getAddress());
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Permanently Unsubscribe'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'Permanently unsubscribe this email address (%s) from this '.
|
||||
'payment account (%s)?',
|
||||
$email_display,
|
||||
$account_display))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'You will no longer receive email and access links will no longer '.
|
||||
'function.'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
'This action is permanent and can not be undone.'))
|
||||
->addCancelButton($email_uri)
|
||||
->addSubmitButton($submit);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -85,6 +85,13 @@ final class PhortuneAccountEmail
|
|||
$this->getAccessKey());
|
||||
}
|
||||
|
||||
public function getUnsubscribeURI() {
|
||||
return urisprintf(
|
||||
'/phortune/external/%s/%s/unsubscribe/',
|
||||
$this->getAddressKey(),
|
||||
$this->getAccessKey());
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneAccountEmailRotateTransaction
|
||||
extends PhortuneAccountEmailTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'rotate';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$access_key = Filesystem::readRandomCharacters(16);
|
||||
$object->setAccessKey($access_key);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s rotated the access key for this email address.',
|
||||
$this->renderAuthor());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
final class PhortuneAccountEmailStatusTransaction
|
||||
extends PhortuneAccountEmailTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'status';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getStatus();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setStatus($value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s changed the status for this address to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue