Merge pull request #50 from flarum/avatar-api

API for uploading avatars
This commit is contained in:
Toby Zerner 2015-03-27 08:49:11 +10:30
commit 0fdbb75914
14 changed files with 281 additions and 5 deletions

View File

@ -0,0 +1,45 @@
import Ember from 'ember';
import config from 'flarum/config/environment';
var $ = Ember.$;
export default Ember.Component.extend({
layoutName: 'components/user/avatar-editor',
classNames: ['avatar-editor', 'dropdown'],
classNameBindings: ['loading'],
click: function(e) {
if (! this.get('user.avatarUrl')) {
e.preventDefault();
e.stopPropagation();
this.send('upload');
}
},
actions: {
upload: function() {
if (this.get('loading')) { return; }
var $input = $('<input type="file">');
var userId = this.get('user.id');
var component = this;
$input.appendTo('body').hide().click().on('change', function() {
var formData = new FormData();
formData.append('avatar', $(this)[0].files[0]);
component.set('loading', true);
$.ajax({
type: 'POST',
url: config.apiURL+'/users/'+userId+'/avatar',
data: formData,
cache: false,
contentType: false,
processData: false,
complete: function() {
component.set('loading', false);
}
});
});
}
}
});

View File

@ -47,10 +47,17 @@
display: inline;
vertical-align: middle;
}
& .avatar {
.avatar-size(96px);
& .user-avatar {
float: left;
margin-left: -130px;
}
& .avatar-editor .dropdown-toggle {
margin: 4px;
line-height: 96px;
font-size: 26px;
}
& .avatar {
.avatar-size(96px);
border: 4px solid #fff;
.box-shadow(0 2px 6px @fl-shadow-color);
}
@ -170,3 +177,30 @@
}
}
}
.avatar-editor {
position: relative;
& .dropdown-toggle {
opacity: 0;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
border-radius: 100%;
background: rgba(0, 0, 0, 0.4);
text-align: center;
text-decoration: none;
}
&:hover .dropdown-toggle, &.open .dropdown-toggle, &.loading .dropdown-toggle {
opacity: 1;
}
& .loading-indicator {
color: #fff;
}
& .dropdown-menu {
left: 35%;
top: 65%;
}
}

View File

@ -0,0 +1,12 @@
{{user-avatar user}}
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
{{#if loading}}
{{ui/loading-indicator}}
{{else}}
{{fa-icon "pencil"}}
{{/if}}
</a>
<ul class="dropdown-menu">
<li><a href="#" {{action "upload"}}>{{fa-icon "upload"}} Upload</a></li>
<li><a href="#" {{action "remove"}}>{{fa-icon "times"}} Remove</a></li>
</ul>

View File

@ -6,7 +6,11 @@
<div class="user-profile">
<h2 class="user-identity">
{{#link-to "user" user}}{{user-avatar user}}{{user-name user}}{{/link-to}}
{{#if editable}}
{{user/avatar-editor user=user class="user-avatar"}}{{user-name user}}
{{else}}
{{#link-to "user" user}}{{user-avatar user class="user-avatar"}}{{user-name user}}{{/link-to}}
{{/if}}
</h2>
{{ui/item-list items=user.badges class="badges user-badges"}}

View File

@ -23,6 +23,7 @@ class CreateUsersTable extends Migration {
$table->string('password');
$table->text('bio')->nullable();
$table->text('bio_html')->nullable();
$table->string('avatar_path')->nullable();
$table->dateTime('join_time')->nullable();
$table->dateTime('last_seen_time')->nullable();
$table->dateTime('read_time')->nullable();

View File

@ -13,8 +13,6 @@ use Response;
abstract class BaseAction extends Action
{
abstract protected function run(ApiParams $params);
public function __construct(Actor $actor, Dispatcher $bus)
{
$this->actor = $actor;
@ -40,6 +38,15 @@ abstract class BaseAction extends Action
return $this->run($params);
}
/**
* @param ApiParams $params
* @return mixed
*/
protected function run(ApiParams $params)
{
// Should be implemented by subclasses
}
public function hydrate($object, $params)
{
foreach ($params as $k => $v) {

View File

@ -0,0 +1,21 @@
<?php namespace Flarum\Api\Actions\Users;
use Flarum\Api\Actions\BaseAction;
use Flarum\Core\Commands\UploadAvatarCommand;
use Illuminate\Http\Request;
class UploadAvatarAction extends BaseAction
{
public function handle(Request $request, $routeParams = [])
{
$userId = array_get($routeParams, 'id');
$file = $request->file('avatar');
$this->dispatch(
new UploadAvatarCommand($userId, $file, $this->actor->getUser()),
$routeParams
);
return $this->respondWithoutContent(201);
}
}

View File

@ -59,6 +59,11 @@ Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWit
'uses' => $action('Flarum\Api\Actions\Users\DeleteAction')
]);
Route::post('users/{id}/avatar', [
'as' => 'flarum.api.users.avatar.upload',
'uses' => $action('Flarum\Api\Actions\Users\UploadAvatarAction')
]);
/*
|--------------------------------------------------------------------------
| Activity

View File

@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Commands;
use RuntimeException;
class UploadAvatarCommand
{
public $userId;
/**
* @var \Symfony\Component\HttpFoundation\File\UploadedFile
*/
public $file;
public $actor;
public function __construct($userId, $file, $actor)
{
if (empty($userId) || !intval($userId)) {
throw new RuntimeException('No valid user ID specified.');
}
if (is_null($file)) {
throw new RuntimeException('No file to upload');
}
$this->userId = $userId;
$this->file = $file;
$this->actor = $actor;
}
}

View File

@ -1,6 +1,7 @@
<?php namespace Flarum\Core;
use Illuminate\Bus\Dispatcher as Bus;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ServiceProvider;
use Flarum\Core\Formatter\FormatterManager;
@ -12,6 +13,7 @@ use Flarum\Core\Models\User;
use Flarum\Core\Models\Discussion;
use Flarum\Core\Models\Notification;
use Flarum\Core\Search\GambitManager;
use League\Flysystem\Adapter\Local;
class CoreServiceProvider extends ServiceProvider
{
@ -83,6 +85,16 @@ class CoreServiceProvider extends ServiceProvider
'Flarum\Core\Repositories\NotificationRepositoryInterface',
'Flarum\Core\Repositories\EloquentNotificationRepository'
);
$this->app->singleton('flarum.avatars.storage', function () {
return new Local(__DIR__.'/../../ember/public/avatars');
});
$this->app->when('Flarum\Core\Handlers\Commands\UploadAvatarCommandHandler')
->needs('League\Flysystem\FilesystemInterface')
->give(function(Container $app) {
return $app->make('Illuminate\Contracts\Filesystem\Factory')->disk('avatars')->getDriver();
});
}
public function registerGambits()

View File

@ -0,0 +1,16 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class AvatarWillBeUploaded
{
public $user;
public $command;
public function __construct(User $user, $command)
{
$this->user = $user;
$this->command = $command;
}
}

View File

@ -0,0 +1,13 @@
<?php namespace Flarum\Core\Events;
use Flarum\Core\Models\User;
class UserAvatarWasChanged
{
public $user;
public function __construct(User $user)
{
$this->user = $user;
}
}

View File

@ -0,0 +1,60 @@
<?php namespace Flarum\Core\Handlers\Commands;
use Flarum\Core\Commands\UploadAvatarCommand;
use Flarum\Core\Events\AvatarWillBeUploaded;
use Flarum\Core\Repositories\UserRepositoryInterface;
use Flarum\Core\Support\DispatchesEvents;
use Illuminate\Support\Str;
use League\Flysystem\Adapter\Local;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\MountManager;
class UploadAvatarCommandHandler
{
use DispatchesEvents;
/**
* @var UserRepositoryInterface
*/
protected $users;
/**
* @var FilesystemInterface
*/
protected $uploadDir;
public function __construct(UserRepositoryInterface $users, FilesystemInterface $uploadDir)
{
$this->users = $users;
$this->uploadDir = $uploadDir;
}
public function handle(UploadAvatarCommand $command)
{
$user = $this->users->findOrFail($command->userId);
// Make sure the current user is allowed to edit the user profile.
// This will let admins and the user themselves pass through, and
// throw an exception otherwise.
$user->assertCan($command->actor, 'edit');
$filename = $command->file->getFilename();
$uploadName = Str::lower(Str::quickRandom()) . '.jpg';
$mount = new MountManager([
'source' => new Filesystem(new Local($command->file->getPath())),
'target' => $this->uploadDir,
]);
$user->changeAvatarPath($uploadName);
event(new AvatarWillBeUploaded($user, $command));
$mount->move("source://$filename", "target://$uploadName");
$user->save();
$this->dispatchEventsFor($user);
return $user;
}
}

View File

@ -10,6 +10,7 @@ use Flarum\Core\Events\UserWasRenamed;
use Flarum\Core\Events\UserEmailWasChanged;
use Flarum\Core\Events\UserPasswordWasChanged;
use Flarum\Core\Events\UserBioWasChanged;
use Flarum\Core\Events\UserAvatarWasChanged;
use Flarum\Core\Events\UserWasActivated;
use Flarum\Core\Events\UserEmailWasConfirmed;
@ -210,6 +211,21 @@ class User extends Model
return $this;
}
/**
* Change the path of the user avatar.
*
* @param string $path
* @return $this
*/
public function changeAvatarPath($path)
{
$this->avatar_path = $path;
$this->raise(new UserAvatarWasChanged($this));
return $this;
}
/**
* Check if a given password matches the user's password.
*