Skip to content

Commit

Permalink
[FEATURE] REST API login (#19)
Browse files Browse the repository at this point in the history
This commit provides the REST bundle and the createSession action.

Also update the REST API documentation so that the property names match
those use in the domain model classes.

It also adds the CI stuff for the new code.
  • Loading branch information
oliverklee authored and Sam Tuke committed Jul 21, 2017
1 parent dbc87a3 commit eaad8ec
Show file tree
Hide file tree
Showing 13 changed files with 4,609 additions and 18 deletions.
45 changes: 39 additions & 6 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,57 @@ have a chance of keeping on top of things:

## Unit-test your changes

Please cover all changes with unit tests and make sure that your code does not
break any existing tests. We will only merge pull request that include full
Please cover all changes with automatic tests and make sure that your code does
not break any existing tests. We will only merge pull request that include full
code coverage of the fixed bugs and the new features.

To run the existing PHPUnit tests, run this command:
### Running the unit tests

vendor/bin/phpunit Tests/
To run the existing unit tests, run this command:

vendor/bin/phpunit -c Configuration/PHPUnit/phpunit.xml Tests/Unit/

### Running the integration tests

For being able to run the integration tests, you will need a local MySQL
database and a user with access permissions to that database.

After you have created the database and the user, please import the database
schema once. Assuming that your database is named `phplist_test`, the user is
named `phplist`, and the password is `batterystaple`, the command looks like
this:

mysql -u phplist_test --password=batterystaple phplist_test < Database/Schema.sql

When running the integration tests, you will need to specify the database name
and access credentials on the command line (in the same line):

PHPLIST_DATABASE_NAME=phplist_test PHPLIST_DATABASE_USER=phplist PHPLIST_DATABASE_PASSWORD=batterystaple vendor/bin/phpunit -c Configuration/PHPUnit/phpunit.xml Tests/Integration/


## Coding Style

Please make your code clean, well-readable and easy to understand.

Please use the same coding style ([PSR-2](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md))
as the rest of the code. Indentation for all files is four spaces.

We will only merge pull requests that follow the project's coding style.

Please check your code with the provided PHP_CodeSniffer standard:

vendor/bin/phpcs --standard=PSR2 Classes/ Tests/
vendor/bin/phpcs --standard=vendor/phplist/phplist4-core/Configuration/PhpCodeSniffer/ Classes/ Tests/

Please make your code clean, well-readable and easy to understand.
Please also check the code structure using PHPMD:

vendor/bin/phpmd Classes/ text vendor/phplist/phplist4-core/Configuration/PHPMD/rules.xml

And also please run the static code analysis:

vendor/bin/phpstan analyse -l 5 Classes/ Tests/

You can also run all code style checks using one long line from a bash shell:
find Classes/ Tests/ -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l && vendor/bin/phpstan analyse -l 5 Classes/ Tests/ && vendor/bin/phpmd Classes/ text vendor/phplist/phplist4-core/Configuration/PHPMD/rules.xml && vendor/bin/phpcs --standard=vendor/phplist/phplist4-core/Configuration/PhpCodeSniffer/ Classes/ Tests/

This will execute all tests except for the unit tests and the integration
tests.
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@
/.idea/
/.project
/.webprj
/composer.lock
/nbproject
/vendor/
40 changes: 38 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ matrix:
services:
- mysql

env:
global:
- PHPLIST_DATABASE_NAME=phplist
- PHPLIST_DATABASE_USER=travis
- PHPLIST_DATABASE_PASSWORD=''

sudo: false
cache:
directories:
Expand All @@ -16,12 +22,42 @@ cache:

before_install:
- phpenv config-rm xdebug.ini
- mysql -e 'CREATE DATABASE phplist;'
- >
echo;
echo "Importing the database schema";
mysql -e "CREATE DATABASE ${PHPLIST_DATABASE_NAME};";
mysql ${PHPLIST_DATABASE_NAME} < vendor/phplist/phplist4-core/Database/Schema.sql;
install:
- composer install

script:
- >
echo;
echo "Nothing to do yet.";
echo "Linting all PHP files";
find Classes/ Tests/ -name '*.php' -print0 | xargs -0 -n 1 -P 4 php -l;
- >
echo;
echo "Running the unit tests";
vendor/bin/phpunit -c Configuration/PHPUnit/phpunit.xml Tests/Unit/;
- >
echo;
echo "Running the integration tests";
vendor/bin/phpunit -c Configuration/PHPUnit/phpunit.xml Tests/Integration/;
- >
echo;
echo "Running the static analysis";
vendor/bin/phpstan analyse -l 5 Classes/ Tests/;
- >
echo;
echo "Running PHPMD";
vendor/bin/phpmd Classes/ text vendor/phplist/phplist4-core/Configuration/PHPMD/rules.xml;
- >
echo;
echo "Running PHP_CodeSniffer";
vendor/bin/phpcs --standard=vendor/phplist/phplist4-core/Configuration/PhpCodeSniffer/ Classes/ Tests/;
156 changes: 156 additions & 0 deletions Classes/Controller/SessionController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);

namespace PhpList\RestBundle\Controller;

use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\ORM\EntityManagerInterface;
use PhpList\PhpList4\Core\Bootstrap;
use PhpList\PhpList4\Domain\Model\Identity\Administrator;
use PhpList\PhpList4\Domain\Model\Identity\AdministratorToken;
use PhpList\PhpList4\Domain\Repository\Identity\AdministratorRepository;
use PhpList\PhpList4\Domain\Repository\Identity\AdministratorTokenRepository;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
* This controller provides methods to create and destroy REST API sessions.
*
* @author Oliver Klee <[email protected]>
*/
class SessionController extends Controller
{
/**
* @var EntityManagerInterface
*/
private $entityManager = null;

/**
* @var AdministratorRepository|ObjectRepository
*/
private $administratorRepository = null;

/**
* @var AdministratorTokenRepository|ObjectRepository
*/
private $administratorTokenRepository = null;

/**
* The constructor.
*/
public function __construct()
{
// This will later be replaced by dependency injection.
$this->entityManager = Bootstrap::getInstance()->getEntityManager();
$this->administratorRepository = $this->entityManager->getRepository(Administrator::class);
$this->administratorTokenRepository = $this->entityManager->getRepository(AdministratorToken::class);
}

/**
* Creates a new session (if the provided credentials are valid).
*
* @param Request $request
*
* @return Response
*/
public function createAction(Request $request): Response
{
$rawRequestContent = $request->getContent();
$response = new Response();
if (!$this->validateCreateRequest($rawRequestContent, $response)) {
return $response;
}

$parsedRequestContent = json_decode($rawRequestContent, true);

$loginName = $parsedRequestContent['loginName'];
$password = $parsedRequestContent['password'];
$administrator = $this->administratorRepository->findOneByLoginCredentials($loginName, $password);
if ($administrator !== null) {
$token = $this->createAndPersistToken($administrator);
$statusCode = 201;
$responseContent = [
'id' => $token->getId(),
'key' => $token->getKey(),
'expiry' => $token->getExpiry()->format(\DateTime::ATOM),
];
} else {
$statusCode = 401;
$responseContent = [
'code' => 1500567098798,
'message' => 'Not authorized',
'description' => 'The user name and password did not match any existing user.',
];
}

$response->setStatusCode($statusCode);
$response->setContent(json_encode($responseContent, JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT));

return $response;
}

/**
* Validated the request. If is it not valid, sets a status code and a response content.
*
* @param string $rawRequestContent
* @param Response $response
*
* @return bool whether the response is valid
*
* @return void
*/
private function validateCreateRequest(string $rawRequestContent, Response $response): bool
{
$parsedRequestContent = json_decode($rawRequestContent, true);
$isValid = false;

if ($rawRequestContent === '') {
$responseContent = [
'code' => 1500559729794,
'message' => 'No data',
'description' => 'The request does not contain any data.',
];
} elseif ($parsedRequestContent === null) {
$responseContent = [
'code' => 1500562402438,
'message' => 'Invalid JSON data',
'description' => 'The data in the request is invalid JSON.',
];
} elseif (empty($parsedRequestContent['loginName']) || empty($parsedRequestContent['password'])) {
$responseContent = [
'code' => 1500562647846,
'message' => 'Incomplete credentials',
'description' => 'The request does not contain both loginName and password.',
];
} else {
$responseContent = [];
$isValid = true;
}

if (!$isValid) {
$response->setStatusCode(500);
$response->setContent(json_encode($responseContent, JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT));
}

return $isValid;
}

/**
* @param Administrator $administrator
*
* @return AdministratorToken
*/
private function createAndPersistToken(Administrator $administrator): AdministratorToken
{
$token = new AdministratorToken();
$token->setAdministrator($administrator);
$token->generateExpiry();
$token->generateKey();

$this->entityManager->persist($token);
$this->entityManager->flush();

return $token;
}
}
15 changes: 15 additions & 0 deletions Classes/PhpListRestBundle.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);

namespace PhpList\RestBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

/**
* This bundle provides the REST API for phpList.
*
* @author Oliver Klee <[email protected]>
*/
class PhpListRestBundle extends Bundle
{
}
14 changes: 14 additions & 0 deletions Configuration/PHPUnit/phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>

<!-- https://phpunit.de/manual/current/en/appendixes.configuration.html -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="../../vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1"/>
<server name="KERNEL_CLASS" value="PhpList\PhpList4\Core\ApplicationKernel"/>
</php>
</phpunit>
14 changes: 7 additions & 7 deletions Documentation/Api/RestApi.apib
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@ Resources related to login and logout.
Given valid login data, this will generate a login token that will be
valid for 1 hour. It takes a JSON object containing the following key-value pairs:

+ `userName` (string): the user name to log in
+ `loginName` (string): the login name ofe the administrator to log in
+ `password` (string): the plain text password

The login token then can be passed as a `loginToken` cookie for request that
require authentication.
The login token then can be passed as a `authenticationToken` cookie for requests
that require authentication.

+ Request (application/json)

{
"message": "Success",
"userName": "admin",
"loginName": "admin",
"password": "eetIc/Gropvoc1"
}

Expand All @@ -35,15 +34,16 @@ require authentication.

{
"id": 241,
"loginToken": "2cfe100561473c6cdd99c9e2f26fa974"
"key": "2cfe100561473c6cdd99c9e2f26fa974",
"expiry": "2017-07-20T18:22:48+00:00"
}

+ Response 401 (application/json)

+ Body

{
"code": 1,
"code": 1500567098798,
"message": "Not authorized",
"description": "The user name and password did not match any existing user."
}
Loading

0 comments on commit eaad8ec

Please sign in to comment.