mirror of
https://github.com/flarum/core.git
synced 2025-07-25 10:41:24 +02:00
Merge pull request #50 from flarum/avatar-api
API for uploading avatars
This commit is contained in:
45
ember/app/components/user/avatar-editor.js
Normal file
45
ember/app/components/user/avatar-editor.js
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
@@ -47,10 +47,17 @@
|
|||||||
display: inline;
|
display: inline;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
& .avatar {
|
& .user-avatar {
|
||||||
.avatar-size(96px);
|
|
||||||
float: left;
|
float: left;
|
||||||
margin-left: -130px;
|
margin-left: -130px;
|
||||||
|
}
|
||||||
|
& .avatar-editor .dropdown-toggle {
|
||||||
|
margin: 4px;
|
||||||
|
line-height: 96px;
|
||||||
|
font-size: 26px;
|
||||||
|
}
|
||||||
|
& .avatar {
|
||||||
|
.avatar-size(96px);
|
||||||
border: 4px solid #fff;
|
border: 4px solid #fff;
|
||||||
.box-shadow(0 2px 6px @fl-shadow-color);
|
.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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
12
ember/app/templates/components/user/avatar-editor.hbs
Normal file
12
ember/app/templates/components/user/avatar-editor.hbs
Normal 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>
|
@@ -6,7 +6,11 @@
|
|||||||
|
|
||||||
<div class="user-profile">
|
<div class="user-profile">
|
||||||
<h2 class="user-identity">
|
<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>
|
</h2>
|
||||||
|
|
||||||
{{ui/item-list items=user.badges class="badges user-badges"}}
|
{{ui/item-list items=user.badges class="badges user-badges"}}
|
||||||
|
@@ -23,6 +23,7 @@ class CreateUsersTable extends Migration {
|
|||||||
$table->string('password');
|
$table->string('password');
|
||||||
$table->text('bio')->nullable();
|
$table->text('bio')->nullable();
|
||||||
$table->text('bio_html')->nullable();
|
$table->text('bio_html')->nullable();
|
||||||
|
$table->string('avatar_path')->nullable();
|
||||||
$table->dateTime('join_time')->nullable();
|
$table->dateTime('join_time')->nullable();
|
||||||
$table->dateTime('last_seen_time')->nullable();
|
$table->dateTime('last_seen_time')->nullable();
|
||||||
$table->dateTime('read_time')->nullable();
|
$table->dateTime('read_time')->nullable();
|
||||||
|
@@ -13,8 +13,6 @@ use Response;
|
|||||||
|
|
||||||
abstract class BaseAction extends Action
|
abstract class BaseAction extends Action
|
||||||
{
|
{
|
||||||
abstract protected function run(ApiParams $params);
|
|
||||||
|
|
||||||
public function __construct(Actor $actor, Dispatcher $bus)
|
public function __construct(Actor $actor, Dispatcher $bus)
|
||||||
{
|
{
|
||||||
$this->actor = $actor;
|
$this->actor = $actor;
|
||||||
@@ -40,6 +38,15 @@ abstract class BaseAction extends Action
|
|||||||
return $this->run($params);
|
return $this->run($params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ApiParams $params
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function run(ApiParams $params)
|
||||||
|
{
|
||||||
|
// Should be implemented by subclasses
|
||||||
|
}
|
||||||
|
|
||||||
public function hydrate($object, $params)
|
public function hydrate($object, $params)
|
||||||
{
|
{
|
||||||
foreach ($params as $k => $v) {
|
foreach ($params as $k => $v) {
|
||||||
|
21
src/Api/Actions/Users/UploadAvatarAction.php
Normal file
21
src/Api/Actions/Users/UploadAvatarAction.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@@ -59,6 +59,11 @@ Route::group(['prefix' => 'api', 'middleware' => 'Flarum\Api\Middleware\LoginWit
|
|||||||
'uses' => $action('Flarum\Api\Actions\Users\DeleteAction')
|
'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
|
| Activity
|
||||||
|
30
src/Core/Commands/UploadAvatarCommand.php
Normal file
30
src/Core/Commands/UploadAvatarCommand.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,7 @@
|
|||||||
<?php namespace Flarum\Core;
|
<?php namespace Flarum\Core;
|
||||||
|
|
||||||
use Illuminate\Bus\Dispatcher as Bus;
|
use Illuminate\Bus\Dispatcher as Bus;
|
||||||
|
use Illuminate\Contracts\Container\Container;
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
use Illuminate\Contracts\Events\Dispatcher;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Flarum\Core\Formatter\FormatterManager;
|
use Flarum\Core\Formatter\FormatterManager;
|
||||||
@@ -12,6 +13,7 @@ use Flarum\Core\Models\User;
|
|||||||
use Flarum\Core\Models\Discussion;
|
use Flarum\Core\Models\Discussion;
|
||||||
use Flarum\Core\Models\Notification;
|
use Flarum\Core\Models\Notification;
|
||||||
use Flarum\Core\Search\GambitManager;
|
use Flarum\Core\Search\GambitManager;
|
||||||
|
use League\Flysystem\Adapter\Local;
|
||||||
|
|
||||||
class CoreServiceProvider extends ServiceProvider
|
class CoreServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -83,6 +85,16 @@ class CoreServiceProvider extends ServiceProvider
|
|||||||
'Flarum\Core\Repositories\NotificationRepositoryInterface',
|
'Flarum\Core\Repositories\NotificationRepositoryInterface',
|
||||||
'Flarum\Core\Repositories\EloquentNotificationRepository'
|
'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()
|
public function registerGambits()
|
||||||
|
16
src/Core/Events/AvatarWillBeUploaded.php
Normal file
16
src/Core/Events/AvatarWillBeUploaded.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
13
src/Core/Events/UserAvatarWasChanged.php
Normal file
13
src/Core/Events/UserAvatarWasChanged.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
60
src/Core/Handlers/Commands/UploadAvatarCommandHandler.php
Normal file
60
src/Core/Handlers/Commands/UploadAvatarCommandHandler.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@@ -10,6 +10,7 @@ use Flarum\Core\Events\UserWasRenamed;
|
|||||||
use Flarum\Core\Events\UserEmailWasChanged;
|
use Flarum\Core\Events\UserEmailWasChanged;
|
||||||
use Flarum\Core\Events\UserPasswordWasChanged;
|
use Flarum\Core\Events\UserPasswordWasChanged;
|
||||||
use Flarum\Core\Events\UserBioWasChanged;
|
use Flarum\Core\Events\UserBioWasChanged;
|
||||||
|
use Flarum\Core\Events\UserAvatarWasChanged;
|
||||||
use Flarum\Core\Events\UserWasActivated;
|
use Flarum\Core\Events\UserWasActivated;
|
||||||
use Flarum\Core\Events\UserEmailWasConfirmed;
|
use Flarum\Core\Events\UserEmailWasConfirmed;
|
||||||
|
|
||||||
@@ -210,6 +211,21 @@ class User extends Model
|
|||||||
return $this;
|
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.
|
* Check if a given password matches the user's password.
|
||||||
*
|
*
|
||||||
|
Reference in New Issue
Block a user