Merge branch 'feature/api' into dev-master

This commit is contained in:
Simon Vieille 2017-09-21 17:56:44 +02:00
commit 9fe6510fc9
26 changed files with 662 additions and 106 deletions

View file

@ -3,5 +3,11 @@
use Gist\Api\Client;
$app['api_client'] = $app->share(function ($app) {
return new Client(['base_uri' => rtrim($app['settings']['api']['base_url'], '/')]);
$client = new Client(['base_uri' => rtrim($app['settings']['api']['base_url'], '/')]);
if (!empty($app['settings']['api']['client']['api_key'])) {
$client->setApiKey($app['settings']['api']['client']['api_key']);
}
return $client;
});

View file

@ -6,7 +6,11 @@ security:
login_required_to_view_gist: false
login_required_to_view_embeded_gist: false
api:
enabled: true
api_key_required: false
base_url: 'https://gist.deblan.org/'
client:
api_key:
data:
path: data/git
git:

View file

@ -57,10 +57,18 @@ revisions:
path: /revs/{gist}
defaults: {_controller: Gist\Controller\ViewController::revisionsAction, _locale: en}
api_list:
path: /api/list/{apiKey}
defaults: {_controller: Gist\Controller\ApiController::listAction, _locale: en, apiKey: null}
api_create:
path: /api/create
defaults: {_controller: Gist\Controller\ApiController::createAction, _locale: en}
path: /api/create/{apiKey}
defaults: {_controller: Gist\Controller\ApiController::createAction, _locale: en, apiKey: null}
api_update:
path: /api/update/{gist}
defaults: {_controller: Gist\Controller\ApiController::updateAction, _locale: en}
path: /api/update/{gist}/{apiKey}
defaults: {_controller: Gist\Controller\ApiController::updateAction, _locale: en, apiKey: null}
api_delete:
path: /api/delete/{gist}/{apiKey}
defaults: {_controller: Gist\Controller\ApiController::deleteAction, _locale: en, apiKey: null}

View file

@ -2,15 +2,19 @@
<?php
use Gist\Command\CreateCommand;
use Gist\Command\ListCommand;
use Gist\Command\UpdateCommand;
use Gist\Command\StatsCommand;
use Gist\Command\DeleteCommand;
use Gist\Command\UserCreateCommand;
use Gist\Command\Migration\UpgradeTo1p4p1Command;
$app = require __DIR__.'/bootstrap.php';
$app['console']->add(new CreateCommand());
$app['console']->add(new ListCommand());
$app['console']->add(new UpdateCommand());
$app['console']->add(new DeleteCommand());
$app['console']->add(new StatsCommand());
$app['console']->add(new UserCreateCommand());
$app['console']->add(new UpgradeTo1p4p1Command());

View file

@ -20,6 +20,11 @@ app:
my:
title: '我的 Gist'
nothing: '这家伙很懒,暂时没有内容'
api:
title: 'API'
warning: 'Keep it <strong>secret!</strong>'
form:
generate: 'Regenerate'
gist:
untitled: '未命名'

View file

@ -20,6 +20,11 @@ app:
my:
title: 'Meine Gists'
nothing: 'Nichts zu finden (momentan)!'
api:
title: 'API'
warning: 'Keep it <strong>secret!</strong>'
form:
generate: 'Regenerate'
gist:
untitled: 'Ohne Titel'

View file

@ -20,6 +20,12 @@ app:
my:
title: 'My gists'
nothing: 'Nothing yet!'
api:
title: 'API'
warning: 'Keep it <strong>secret!</strong>'
form:
generate: 'Regenerate'
gist:
untitled: 'Untitled'

View file

@ -20,6 +20,11 @@ app:
my:
title: 'Mis Gists'
nothing: 'Nada por ahora.'
api:
title: 'API'
warning: 'Keep it <strong>secret!</strong>'
form:
generate: 'Regenerate'
gist:
untitled: 'Sin título'

View file

@ -20,6 +20,11 @@ app:
my:
title: 'Mes Gists'
nothing: 'Rien pour le moment !'
api:
title: 'API'
warning: 'Gardez-la <strong>secrète !</strong>'
form:
generate: 'Regénérer'
gist:
untitled: 'Sans titre'

View file

@ -12,31 +12,53 @@ use GuzzleHttp\Client as BaseClient;
class Client extends BaseClient
{
/**
* URI of creation
*
* URI of creation.
*
* @const string
*/
const CREATE = '/en/api/create';
/**
* URI of updating
* URI of update.
*
* @const string
*/
const UPDATE = '/en/api/update/{gist}';
/**
* Creates a gist
* URI of delete.
*
* @param string $title The title
* @param string $type The type
* @const string
*/
const DELETE = '/en/api/delete/{gist}';
/**
* URI of list.
*
* @const string
*/
const LIST = '/en/api/list';
/**
* The API key.
*
* @var string|null
*/
protected $apiKey;
/**
* Creates a gist.
*
* @param string $title The title
* @param string $type The type
* @param string $content The content
*
* @return array
*/
public function create($title, $type, $content)
{
$response = $this->post(
self::CREATE,
$this->mergeApiKey(self::CREATE),
array(
'form_params' => array(
'form' => array(
@ -56,9 +78,9 @@ class Client extends BaseClient
}
/**
* Clones and update a gist
* Clones and update a gist.
*
* @param string $gist Gist's ID
* @param string $gist Gist's ID
* @param string $content The content
*
* @return array
@ -66,7 +88,7 @@ class Client extends BaseClient
public function update($gist, $content)
{
$response = $this->post(
str_replace('{gist}', $gist, self::UPDATE),
str_replace('{gist}', $gist, $this->mergeApiKey(self::LIST)),
array(
'form_params' => array(
'form' => array(
@ -82,4 +104,81 @@ class Client extends BaseClient
return [];
}
/**
* Deletes a gist.
*
* @param string $gist Gist's ID
*
* @return array
*/
public function delete($gist)
{
$response = $this->post(str_replace('{gist}', $gist, $this->mergeApiKey(self::DELETE)));
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody()->getContents(), true);
}
return [];
}
/**
* Lists the user's gists.
*
* @param string $gist Gist's ID
* @param string $content The content
*
* @return array
*/
public function list()
{
$response = $this->get($this->mergeApiKey(self::LIST));
if ($response->getStatusCode() === 200) {
return json_decode($response->getBody()->getContents(), true);
}
return [];
}
/*
* Merges the API key with the given url.
*
* @param string $url
*
* @return string
*/
public function mergeApiKey($url)
{
if (empty($this->apiKey)) {
return $url;
}
return rtrim($url, '/').'/'.$this->apiKey;
}
/*
* Set the value of "apiKey".
*
* @param string|null $apiKey
*
* @return Client
*/
public function setApiKey($apiKey)
{
$this->apiKey = $apiKey;
return $this;
}
/*
* Get the value of "apiKey".
*
* @return string|null
*/
public function getApiKey()
{
return $this->apiKey;
}
}

View file

@ -7,6 +7,8 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Propel\Runtime\Parser\YamlParser;
use Symfony\Component\Yaml\Yaml;
/**
* class CreateCommand.
@ -27,8 +29,9 @@ class CreateCommand extends Command
->addArgument('input', InputArgument::REQUIRED, 'Input')
->addArgument('type', InputArgument::OPTIONAL, 'Type', 'text')
->addOption('title', 't', InputOption::VALUE_REQUIRED, 'Title of the gist')
->addOption('show-url', 'u', InputOption::VALUE_NONE, 'Display only the gist url')
->addOption('show-id', 'i', InputOption::VALUE_NONE, 'Display only the gist Id')
->addOption('all', 'a', InputOption::VALUE_NONE, 'Display all the response')
->addOption('id', 'i', InputOption::VALUE_NONE, 'Display only the id of the gist')
->addOption('json', 'j', InputOption::VALUE_NONE, 'Format the response to json')
->setHelp(<<<EOF
Provides a client to create a gist using the API.
@ -43,12 +46,15 @@ Arguments:
Options:
<info>--title</info>, <info>-t</info>
Defines a title
<info>--show-id</info>, <info>-i</info>
Display only the Id of the gist
<info>--show-url</info>, <info>-u</info>
Display only the url of the gist
<info>--id</info>, <info>-i</info>
Display only the id of the gist
<info>--all</info>, <info>-a</info>
Display all the response
<info>--json</info>, <info>-j</info>
Format the response to json
EOF
);
}
@ -58,11 +64,10 @@ EOF
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
//$output->writeln(sprintf('<comment>%s</comment> bar.', 'test'));
$file = $input->getArgument('input');
$type = $input->getArgument('type');
$title = $input->getOption('title');
$json = $input->getOption('json');
if ($file === '-') {
$content = file_get_contents('php://stdin');
@ -94,19 +99,17 @@ EOF
$gist = $this->getSilexApplication()['api_client']->create($title, $type, $content);
if ($input->getOption('show-url')) {
$output->writeln($gist['url']);
return true;
if ($input->getOption('id')) {
$id = isset($gist['gist']['id']) ? $gist['gist']['id'] : $gist['gist']['Id'];
$result = $json ? json_encode(array('id' => $id)) : $id;
} elseif ($input->getOption('all')) {
$result = $json ? json_encode($gist) : Yaml::dump($gist);
} else {
$url = $gist['url'];
$result = $json ? json_encode(array('url' => $url)) : $url;
}
if ($input->getOption('show-id')) {
$output->writeln($gist['gist']['Id']);
return true;
}
$output->writeln(json_encode($gist));
$output->writeln($result);
}
/**

View file

@ -0,0 +1,48 @@
<?php
namespace Gist\Command;
use Knp\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
/**
* class DeleteCommand.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class DeleteCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('delete')
->setDescription('Delete a gist using the API')
->addOption('gist', null, InputOption::VALUE_REQUIRED, 'Id or File of the gist')
->setHelp(<<<'EOF'
Provides a client to delete a gist using the API.
Arguments:
none.
Options:
<info>--gist</info>
Defines the Gist to delete by using its Id or its File
EOF
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$result = $this->getSilexApplication()['api_client']->delete($input->getOption('gist'));
$output->writeln(empty($result['error']) ? 'OK' : '<error>An error occured.</error>');
}
}

View file

@ -0,0 +1,59 @@
<?php
namespace Gist\Command;
use Knp\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Propel\Runtime\Parser\YamlParser;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Console\Helper\Table;
use DateTime;
/**
* class ListCommand.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class ListCommand extends Command
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this
->setName('gists')
->setDescription('List gists using the API');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$gists = $this->getSilexApplication()['api_client']->list();
$rows = [];
foreach ($gists as $gist) {
$rows[] = array(
$gist['id'],
$gist['title'],
$gist['cipher'] ? 'y' : 'n',
$gist['type'],
(new DateTime($gist['createdAt']))->format('Y-m-d H:i:s'),
(new DateTime($gist['updatedAt']))->format('Y-m-d H:i:s'),
$gist['url'],
);
}
$table = new Table($output);
$table
->setHeaders(array('ID', 'Title', 'Cipher', 'Type', 'Created At', 'Updated At', 'url'))
->setRows($rows);
$table->render();
}
}

View file

@ -7,6 +7,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Yaml\Yaml;
/**
* class UpdateCommand.
@ -26,8 +27,9 @@ class UpdateCommand extends Command
->setDescription('Update a gist using the API')
->addArgument('input', InputArgument::REQUIRED, 'Input')
->addOption('gist', null, InputOption::VALUE_REQUIRED, 'Id or File of the gist')
->addOption('show-url', 'u', InputOption::VALUE_NONE, 'Display only the gist url')
->addOption('show-id', 'i', InputOption::VALUE_NONE, 'Display only the gist Id')
->addOption('all', 'a', InputOption::VALUE_NONE, 'Display all the response')
->addOption('id', 'i', InputOption::VALUE_NONE, 'Display only the id of the gist')
->addOption('json', 'j', InputOption::VALUE_NONE, 'Format the response to json')
->setHelp(<<<EOF
Provides a client to create a gist using the API.
@ -43,11 +45,14 @@ Options:
<info>--gist</info>
Defines the Gist to update by using its Id or its File
<info>--show-id</info>, <info>-i</info>
Display only the Id of the gist
<info>--id</info>, <info>-i</info>
Display only the id of the gist
<info>--show-url</info>, <info>-u</info>
Display only the url of the gist
<info>--all</info>, <info>-a</info>
Display all the response
<info>--json</info>, <info>-j</info>
Format the response to json
EOF
);
}
@ -57,10 +62,9 @@ EOF
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
//$output->writeln(sprintf('<comment>%s</comment> bar.', 'test'));
$file = $input->getArgument('input');
$gist = $input->getOption('gist');
$json = $input->getOption('json');
if ($file === '-') {
$content = file_get_contents('php://stdin');
@ -86,19 +90,17 @@ EOF
$gist = $this->getSilexApplication()['api_client']->update($gist, $content);
if ($input->getOption('show-url')) {
$output->writeln($gist['url']);
return true;
if ($input->getOption('id')) {
$id = isset($gist['gist']['id']) ? $gist['gist']['id'] : $gist['gist']['Id'];
$result = $json ? json_encode(array('id' => $id)) : $id;
} elseif ($input->getOption('all')) {
$result = $json ? json_encode($gist) : Yaml::dump($gist);
} else {
$url = $gist['url'];
$result = $json ? json_encode(array('url' => $url)) : $url;
}
if ($input->getOption('show-id')) {
$output->writeln($gist['gist']['Id']);
return true;
}
$output->writeln(json_encode($gist));
$output->writeln($result);
}
/**

View file

@ -8,6 +8,8 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Gist\Form\ApiCreateGistForm;
use Gist\Model\GistQuery;
use Gist\Form\ApiUpdateGistForm;
use GitWrapper\GitException;
use Gist\Model\UserQuery;
/**
* Class ApiController.
@ -16,10 +18,75 @@ use Gist\Form\ApiUpdateGistForm;
*/
class ApiController extends Controller
{
public function createAction(Request $request)
/**
* Lists gists.
*
* @param Request $request
* @param string $apiKey
*
* @return JsonResponse
*/
public function listAction(Request $request, $apiKey)
{
$app = $this->getApp();
if (false === $app['settings']['api']['enabled']) {
return new Response('', 403);
}
if (false === $this->isValidApiKey($apiKey, true)) {
return $this->invalidApiKeyResponse();
}
if (false === $request->isMethod('get')) {
return $this->invalidMethodResponse('GET method is required.');
}
$user = $app['user.provider']->loadUserByApiKey($apiKey);
$gists = $user->getGists();
$data = array();
foreach ($gists as $gist) {
try {
$history = $app['gist']->getHistory($gist);
$value = $gist->toArray();
$value['url'] = $request->getSchemeAndHttpHost().$app['url_generator']->generate(
'view',
array(
'gist' => $gist->getFile(),
'commit' => array_pop($history)['commit'],
)
);
$data[] = $value;
} catch (GitException $e) {
}
}
return new JsonResponse($data);
}
/**
* Creates a gist.
*
* @param Request $request
* @param string $apiKey
*
* @return JsonResponse
*/
public function createAction(Request $request, $apiKey)
{
$app = $this->getApp();
if (false === $app['settings']['api']['enabled']) {
return new Response('', 403);
}
if (false === $this->isValidApiKey($apiKey, (bool) $app['settings']['api']['api_key_required'])) {
return $this->invalidApiKeyResponse();
}
if (false === $request->isMethod('post')) {
return $this->invalidMethodResponse('POST method is required.');
}
@ -36,30 +103,51 @@ class ApiController extends Controller
$form->submit($request);
if ($form->isValid()) {
$user = !empty($apiKey) ? $app['user.provider']->loadUserByApiKey($apiKey) : null;
$gist = $app['gist']->create(new Gist(), $form->getData());
$gist->setCipher(false)->save();
$gist
->setCipher(false)
->setUser($user)
->save();
$history = $app['gist']->getHistory($gist);
return new JsonResponse(array(
'url' => $request->getSchemeAndHttpHost().$app['url_generator']->generate(
'view',
array(
'gist' => $gist->getFile(),
'commit' => array_pop($history)['commit'],
)
),
'gist' => $gist->toArray(),
));
$data = $gist->toArray();
$data['url'] = $request->getSchemeAndHttpHost().$app['url_generator']->generate(
'view',
array(
'gist' => $gist->getFile(),
'commit' => array_pop($history)['commit'],
)
);
return new JsonResponse($data);
}
return $this->invalidRequestResponse('Invalid field(s)');
}
public function updateAction(Request $request, $gist)
/**
* Updates a gist.
*
* @param Request $request
* @param string $gist
* @param string $apiKey
*
* @return JsonResponse
*/
public function updateAction(Request $request, $gist, $apiKey)
{
$app = $this->getApp();
if (false === $app['settings']['api']['enabled']) {
return new Response('', 403);
}
if (false === $this->isValidApiKey($apiKey, (bool) $app['settings']['api']['api_key_required'])) {
return $this->invalidApiKeyResponse();
}
if (false === $request->isMethod('post')) {
return $this->invalidMethodResponse('POST method is required.');
}
@ -91,21 +179,88 @@ class ApiController extends Controller
$history = $app['gist']->getHistory($gist);
return new JsonResponse(array(
'url' => $request->getSchemeAndHttpHost().$app['url_generator']->generate(
'view',
array(
'gist' => $gist->getFile(),
'commit' => array_pop($history)['commit'],
)
),
'gist' => $gist->toArray(),
));
$data = $gist->toArray();
$data['url'] = $request->getSchemeAndHttpHost().$app['url_generator']->generate(
'view',
array(
'gist' => $gist->getFile(),
'commit' => array_pop($history)['commit'],
)
);
return new JsonResponse($data);
}
return $this->invalidRequestResponse('Invalid field(s)');
}
/**
* Deletes a gist.
*
* @param Request $request
* @param string $gist
* @param string $apiKey
*
* @return JsonResponse
*/
public function deleteAction(Request $request, $gist, $apiKey)
{
$app = $this->getApp();
if (false === $app['settings']['api']['enabled']) {
return new Response('', 403);
}
if (false === $this->isValidApiKey($apiKey, true)) {
return $this->invalidApiKeyResponse();
}
if (false === $request->isMethod('post')) {
return $this->invalidMethodResponse('POST method is required.');
}
$user = $app['user.provider']->loadUserByApiKey($apiKey);
$gist = GistQuery::create()
->filterById((int) $gist)
->_or()
->filterByFile($gist)
->filterByUser($user)
->findOne();
if (!$gist) {
return $this->invalidRequestResponse('Invalid Gist');
}
$gist->delete();
return new JsonResponse(['error' => false]);
}
/**
* Builds an invalid api key response.
*
* @param mixed $message
*
* @return JsonResponse
*/
protected function invalidApiKeyResponse()
{
$data = [
'error' => ' Unauthorized',
'message' => 'Invalid API KEY',
];
return new JsonResponse($data, 401);
}
/**
* Builds an invalid method response.
*
* @param mixed $message
*
* @return JsonResponse
*/
protected function invalidMethodResponse($message = null)
{
$data = [
@ -116,6 +271,13 @@ class ApiController extends Controller
return new JsonResponse($data, 405);
}
/**
* Builds an invalid request response.
*
* @param mixed $message
*
* @return JsonResponse
*/
protected function invalidRequestResponse($message = null)
{
$data = [
@ -125,4 +287,24 @@ class ApiController extends Controller
return new JsonResponse($data, 400);
}
/**
* Checks if the given api key is valid
* depending of the requirement.
*
* @param mixed $apiKey
* @param mixed $required
*
* @return bool
*/
protected function isValidApiKey($apiKey, $required = false)
{
if (empty($apiKey)) {
return !$required;
}
return UserQuery::create()
->filterByApiKey($apiKey)
->count() === 1;
}
}

View file

@ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\Response;
*
* @author Simon Vieille <simon@deblan.fr>
*/
class Controller
abstract class Controller
{
/**
* @var Application
@ -128,12 +128,18 @@ class Controller
/**
* Returns the connected user.
*
* @param Request $request An API request
*
* @return mixed
*/
public function getUser()
public function getUser(Request $request = null)
{
$app = $this->getApp();
if (!empty($request)) {
}
$securityContext = $app['security.token_storage'];
$securityToken = $securityContext->getToken();

View file

@ -58,6 +58,26 @@ class MyController extends Controller
$gists = $this->getUser()->getGistsPager($page, $options);
$apiKey = $this->getUser()->getApiKey();
if (empty($apiKey)) {
$regenerateApiKey = true;
}
// FIXME: CSRF issue!
elseif ($request->request->get('apiKey') === $apiKey && $request->request->has('generateApiKey')) {
$regenerateApiKey = true;
} else {
$regenerateApiKey = false;
}
if ($regenerateApiKey) {
$apiKey = $app['salt_generator']->generate(32, true);
$this->getUser()
->setApiKey($apiKey)
->save();
}
if ($request->isMethod('post')) {
$deleteForm->handleRequest($request);
$passwordForm->handleRequest($request);
@ -104,6 +124,7 @@ class MyController extends Controller
array(
'gists' => $gists,
'page' => $page,
'apiKey' => $apiKey,
'deleteForm' => $deleteForm->createView(),
'filterForm' => $filterForm->createView(),
'passwordForm' => $passwordForm->createView(),

View file

@ -16,7 +16,9 @@ class ApiCreateGistForm extends CreateGistForm
{
parent::build($options);
$this->builder->remove('cipher');
$this->builder
->remove('cipher')
->remove('file');
return $this->builder;
}

View file

@ -18,6 +18,7 @@ class ApiUpdateGistForm extends ApiCreateGistForm
$this->builder
->remove('title')
->remove('file')
->remove('type');
return $this->builder;

View file

@ -3,6 +3,7 @@
namespace Gist\Model;
use Gist\Model\Base\Gist as BaseGist;
use Propel\Runtime\Map\TableMap;
/**
* Class Gist.
@ -86,4 +87,25 @@ class Gist extends BaseGist
return $this;
}
/**
* {@inheritdoc}
*/
public function toArray($keyType = TableMap::TYPE_PHPNAME, $includeLazyLoadColumns = true, $alreadyDumpedObjects = array(), $includeForeignObjects = false)
{
$data = parent::toArray(
$keyType,
$includeLazyLoadColumns,
$alreadyDumpedObjects,
$includeForeignObjects
);
foreach ($data as $key => $value) {
$newKey = lcfirst($key);
unset($data[$key]);
$data[$newKey] = $value;
}
return $data;
}
}

View file

@ -54,7 +54,7 @@ class User extends BaseUser implements UserInterface
*
* @return Propel\Runtime\Util\PropelModelPager
*/
public function getGistsPager($page, $options = array(), $maxPerPage = 10)
public function getGistsPager($page, $options = array(), $maxPerPage = 10)
{
$query = GistQuery::create()
->filterByUser($this)
@ -63,11 +63,11 @@ class User extends BaseUser implements UserInterface
if (!empty($options['type']) && $options['type'] !== 'all') {
$query->filterByType($options['type']);
}
if (!empty($options['title'])) {
$query->filterByTitle('%'.$options['title'].'%', Criteria::LIKE);
}
if (!empty($options['cipher']) && $options['cipher'] !== 'anyway') {
$bools = array(
'yes' => true,

View file

@ -22,6 +22,7 @@
<column name="password" type="VARCHAR" size="255" required="true" />
<column name="roles" type="VARCHAR" size="255" required="true" />
<column name="salt" type="VARCHAR" size="64" required="true" />
<column name="api_key" type="VARCHAR" size="32" required="true" />
<behavior name="timestampable"/>
</table>

View file

@ -205,30 +205,61 @@
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
{{ 'login.login.form.password.placeholder'|trans }}
</div>
<div class="panel-body">
<div class="tab-content">
<form action="{{ path('my', params) }}" method="post">
<p>
{{ form_errors(passwordForm.currentPassword) }}
{{ form_widget(passwordForm.currentPassword) }}
</p>
{% set apiEnabled = app.settings.api.enabled %}
<p>
{{ form_errors(passwordForm.newPassword) }}
{{ form_widget(passwordForm.newPassword) }}
</p>
<div class="row">
<div class="col-md-{{ apiEnabled ? 6 : 12 }}">
<div class="panel panel-default">
<div class="panel-heading">
{{ 'login.login.form.password.placeholder'|trans }}
</div>
<div class="panel-body">
<div class="tab-content">
<form action="{{ path('my', params) }}" method="post">
<p>
{{ form_errors(passwordForm.currentPassword) }}
{{ form_widget(passwordForm.currentPassword) }}
</p>
<p>
{{ form_rest(passwordForm) }}
<input type="submit" class="btn btn-primary" value="{{ 'form.submit'|trans }}">
</p>
</form>
<p>
{{ form_errors(passwordForm.newPassword) }}
{{ form_widget(passwordForm.newPassword) }}
</p>
<p>
{{ form_rest(passwordForm) }}
<input type="submit" class="btn btn-primary" value="{{ 'form.submit'|trans }}">
</p>
</form>
</div>
</div>
</div>
</div>
{% if apiEnabled %}
<div class="col-md-6">
<div class="panel panel-default">
<div class="panel-heading">
{{ 'my.api.title'|trans }}
</div>
<div class="panel-body">
<div class="tab-content">
<p>{{ 'my.api.warning'|trans|raw }}</p>
<form action="{{ path('my', params) }}" method="post">
<div class="row">
<p class="col-md-12">
<input type="text" name="apiKey" id="form-api-key" class="form-control" value="{{ apiKey }}" data-key="{{ apiKey }}">
</p>
<p class="col-md-12">
<input type="submit" name="generateApiKey" value="{{ 'my.api.form.generate'|trans }}" class="btn btn-primary">
</p>
</div>
</form>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>

View file

@ -18,18 +18,30 @@ class SaltGenerator
*
* @return string
*/
public function generate($length = 32)
public function generate($length = 32, $isApiKey = false)
{
if (!is_numeric($length)) {
throw new InvalidArgumentException('Paramter length must be a valid integer.');
}
if (function_exists('openssl_random_pseudo_bytes')) {
return substr(base64_encode(openssl_random_pseudo_bytes($length)), 0, $length);
$string = base64_encode(openssl_random_pseudo_bytes(256));
}
if (function_exists('mcrypt_create_iv')) {
return substr(base64_encode(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM)), 0, $length);
$string = base64_encode(mcrypt_create_iv(256, MCRYPT_DEV_URANDOM));
}
if (!empty($string)) {
if (true === $isApiKey) {
$string = str_replace(
array('+', '%', '/', '#', '&'),
'',
$string
);
}
return substr($string, 0, $length);
}
throw new RuntimeException('You must enable openssl or mcrypt modules.');

View file

@ -126,6 +126,7 @@ class UserProvider implements UserProviderInterface
$user
->setRoles('ROLE_USER')
->setPassword($this->encoder->encodePassword($password, $user->getSalt()))
->setApiKey($this->saltGenerator->generate(32, true))
->save();
return $user;
@ -166,6 +167,20 @@ class UserProvider implements UserProviderInterface
return $user;
}
/**
* Loads a user by his api key.
*
* @param string $apiKey
*
* @return User
*/
public function loadUserByApiKey($apiKey)
{
$user = UserQuery::create()->findOneByApiKey($apiKey);
return $user;
}
/*
* Checks if the given password is the current user password.
*

View file

@ -98,6 +98,10 @@ var myEvents = function() {
$('#form-deletion form').submit();
}
});
$(document).on('change keyup keydown', '#form-api-key', function() {
$(this).val($(this).data('key'));
});
}
var mainEditorEvents = function() {