mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-01-23 14:48:36 +01:00
Add User management features to back-end
This commit is contained in:
parent
37e4711071
commit
96f883d19a
200
app/Api/v1/Controllers/UserManagerController.php
Normal file
200
app/Api/v1/Controllers/UserManagerController.php
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Api\v1\Controllers;
|
||||||
|
|
||||||
|
use App\Api\v1\Requests\UserManagerStoreRequest;
|
||||||
|
use App\Api\v1\Requests\UserManagerUpdateRequest;
|
||||||
|
use App\Api\v1\Resources\UserManagerResource;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Password;
|
||||||
|
use Laravel\Passport\TokenRepository;
|
||||||
|
|
||||||
|
class UserManagerController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display all users.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
return UserManagerResource::collection(User::all());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a user
|
||||||
|
*
|
||||||
|
* @return \App\Api\v1\Resources\UserManagerResource
|
||||||
|
*/
|
||||||
|
public function show(User $user)
|
||||||
|
{
|
||||||
|
return new UserManagerResource($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset user's password
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function resetPassword(Request $request, User $user)
|
||||||
|
{
|
||||||
|
Log::info(sprintf('Password reset for User ID #%s requested by User ID #%s', $user->id, $request->user()->id));
|
||||||
|
|
||||||
|
$credentials = [
|
||||||
|
'token' => $this->broker()->createToken($user),
|
||||||
|
'email' => $user->email,
|
||||||
|
'password' => $user->password,
|
||||||
|
];
|
||||||
|
|
||||||
|
$response = $this->broker()->reset(
|
||||||
|
$credentials, function ($user) {
|
||||||
|
$user->resetPassword();
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($response == Password::PASSWORD_RESET) {
|
||||||
|
Log::info(sprintf('Temporary password set for User ID #%s', $user->id));
|
||||||
|
|
||||||
|
$response = $this->broker()->sendResetLink(
|
||||||
|
['email' => $credentials['email']]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'bad request',
|
||||||
|
'reason' => is_string($response) ? __($response) : __('errors.no_pwd_reset_for_this_user_type')
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response == Password::RESET_LINK_SENT
|
||||||
|
? new UserManagerResource($user)
|
||||||
|
: response()->json([
|
||||||
|
'message' => 'bad request',
|
||||||
|
'reason' => __($response)
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created user in storage.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function store(UserManagerStoreRequest $request)
|
||||||
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => $validated['name'],
|
||||||
|
'email' => $validated['email'],
|
||||||
|
'password' => Hash::make($validated['password']),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::info(sprintf('User ID #%s created by user ID #%s', $user->id, $request->user()->id));
|
||||||
|
|
||||||
|
if ($validated['is_admin']) {
|
||||||
|
$user->promoteToAdministrator();
|
||||||
|
$user->save();
|
||||||
|
Log::notice(sprintf('User ID #%s set as administrator at creation by user ID #%s', $user->id, $request->user()->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->refresh();
|
||||||
|
|
||||||
|
return (new UserManagerResource($user))
|
||||||
|
->response()
|
||||||
|
->setStatusCode(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge user's PATs.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function revokePATs(Request $request, User $user, TokenRepository $tokenRepository)
|
||||||
|
{
|
||||||
|
Log::info(sprintf('Deletion of all personal access tokens for User ID #%s requested by User ID #%s', $user->id, $request->user()->id));
|
||||||
|
|
||||||
|
$tokens = $tokenRepository->forUser($user->getAuthIdentifier());
|
||||||
|
|
||||||
|
$tokens->load('client')->filter(function ($token) {
|
||||||
|
return $token->client->personal_access_client && ! $token->revoked;
|
||||||
|
})->each(function ($token) {
|
||||||
|
$token->revoke();
|
||||||
|
});
|
||||||
|
|
||||||
|
Log::info(sprintf('All personal access tokens for User ID #%s have been revoked', $user->id));
|
||||||
|
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purge user's webauthn credentials.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function revokeWebauthnCredentials(Request $request, User $user)
|
||||||
|
{
|
||||||
|
Log::info(sprintf('Deletion of all security devices for User ID #%s requested by User ID #%s', $user->id, $request->user()->id));
|
||||||
|
|
||||||
|
$user->flushCredentials();
|
||||||
|
|
||||||
|
// WebauthnOnly user options need to be reset to prevent impossible login when
|
||||||
|
// no more registered device exists.
|
||||||
|
// See #110
|
||||||
|
if (blank($user->webAuthnCredentials()->WhereEnabled()->get())) {
|
||||||
|
$user['preferences->useWebauthnOnly'] = false;
|
||||||
|
$user->save();
|
||||||
|
Log::notice(sprintf('No more Webauthn credential for user ID #%s, useWebauthnOnly user preference reset to false', $user->id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Log::info(sprintf('All security devices for User ID #%s have been revoked', $user->id));
|
||||||
|
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified user from storage.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function destroy(Request $request, User $user)
|
||||||
|
{
|
||||||
|
// This will delete the user and all its 2FAs & Groups thanks to the onCascadeDelete constrains.
|
||||||
|
// Deletion will not be done (and returns False) if the user is the only existing admin (see UserObserver clas)
|
||||||
|
return $user->delete() === false
|
||||||
|
? response()->json([
|
||||||
|
'message' => __('errors.cannot_delete_the_only_admin'),
|
||||||
|
], 403)
|
||||||
|
: response()->json(null, 204);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a user
|
||||||
|
*
|
||||||
|
* @return \App\Api\v1\Resources\UserManagerResource
|
||||||
|
*/
|
||||||
|
public function update(UserManagerUpdateRequest $request, User $user)
|
||||||
|
{
|
||||||
|
$user->promoteToAdministrator($request->validated('is_admin'));
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
Log::info(sprintf('User ID #%s set is_admin=%s for User ID #%s', $request->user()->id, $user->isAdministrator(), $user->id));
|
||||||
|
|
||||||
|
return new UserManagerResource($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the broker to be used during password reset.
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Contracts\Auth\PasswordBroker|\Illuminate\Auth\Passwords\PasswordBroker
|
||||||
|
*/
|
||||||
|
protected function broker()
|
||||||
|
{
|
||||||
|
return Password::broker();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
34
app/Api/v1/Requests/UserManagerStoreRequest.php
Normal file
34
app/Api/v1/Requests/UserManagerStoreRequest.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Api\v1\Requests;
|
||||||
|
|
||||||
|
use App\Http\Requests\UserStoreRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class UserManagerStoreRequest extends UserStoreRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return Auth::user()->isAdministrator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
parent::rules(),
|
||||||
|
[
|
||||||
|
'is_admin' => 'required|boolean',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
31
app/Api/v1/Requests/UserManagerUpdateRequest.php
Normal file
31
app/Api/v1/Requests/UserManagerUpdateRequest.php
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Api\v1\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class UserManagerUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return Auth::user()->isAdministrator();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_admin' => 'required|boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
99
app/Api/v1/Resources/UserManagerResource.php
Normal file
99
app/Api/v1/Resources/UserManagerResource.php
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Api\v1\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Laravel\Passport\TokenRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property mixed $id
|
||||||
|
* @property string $name
|
||||||
|
* @property string $email
|
||||||
|
* @property string $oauth_provider
|
||||||
|
* @property \Illuminate\Support\Collection<array-key, mixed> $preferences
|
||||||
|
* @property string $is_admin
|
||||||
|
* @property string $last_seen_at
|
||||||
|
* @property string $created_at
|
||||||
|
* @property string $updated_at
|
||||||
|
* @property int|null $twofaccounts_count
|
||||||
|
*/
|
||||||
|
class UserManagerResource extends UserResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The "data" wrapper that should be applied.
|
||||||
|
*
|
||||||
|
* @var string|null
|
||||||
|
*/
|
||||||
|
public static $wrap = 'info';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new resource instance.
|
||||||
|
*
|
||||||
|
* @param mixed $resource
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($resource)
|
||||||
|
{
|
||||||
|
$this->resource = $resource;
|
||||||
|
$password_reset = null;
|
||||||
|
|
||||||
|
// Password reset token
|
||||||
|
$resetToken = DB::table(config('auth.passwords.users.table'))->where(
|
||||||
|
'email', $this->resource->getEmailForPasswordReset()
|
||||||
|
)->first();
|
||||||
|
|
||||||
|
if ($resetToken) {
|
||||||
|
$password_reset = $this->tokenExpired($resetToken->created_at)
|
||||||
|
? 0
|
||||||
|
: $resetToken->created_at;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Personal Access Tokens (PATs)
|
||||||
|
$tokenRepository = App::make(TokenRepository::class);
|
||||||
|
$tokens = $tokenRepository->forUser($this->resource->getAuthIdentifier());
|
||||||
|
|
||||||
|
$PATs_count = $tokens->load('client')->filter(function ($token) {
|
||||||
|
return $token->client->personal_access_client && ! $token->revoked;
|
||||||
|
})->count();
|
||||||
|
|
||||||
|
$this->with = [
|
||||||
|
'password_reset' => $password_reset,
|
||||||
|
'valid_personal_access_tokens' => $PATs_count,
|
||||||
|
'webauthn_credentials' => $this->resource->webAuthnCredentials()->count()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the token has expired.
|
||||||
|
*
|
||||||
|
* @param string $createdAt
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function tokenExpired($createdAt)
|
||||||
|
{
|
||||||
|
// See Illuminate\Auth\Passwords\DatabaseTokenRepository
|
||||||
|
return Carbon::parse($createdAt)->addSeconds(config('auth.passwords.users.expires', 60)*60)->isPast();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
return array_merge(
|
||||||
|
parent::toArray($request),
|
||||||
|
[
|
||||||
|
'twofaccounts_count' => is_null($this->twofaccounts_count) ? 0 : $this->twofaccounts_count,
|
||||||
|
'last_seen_at' => Carbon::parse($this->last_seen_at)->toDateString(),
|
||||||
|
'created_at' => Carbon::parse($this->created_at)->toDateString(),
|
||||||
|
'updated_at' => Carbon::parse($this->updated_at)->toDateString(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -3,10 +3,12 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Models\Traits\WebAuthnManageCredentials;
|
use App\Models\Traits\WebAuthnManageCredentials;
|
||||||
|
use Illuminate\Auth\Events\PasswordReset;
|
||||||
use Illuminate\Auth\Notifications\ResetPassword;
|
use Illuminate\Auth\Notifications\ResetPassword;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laragear\WebAuthn\WebAuthnAuthentication;
|
use Laragear\WebAuthn\WebAuthnAuthentication;
|
||||||
@ -108,6 +110,18 @@ public function promoteToAdministrator(bool $promote = true)
|
|||||||
$this->is_admin = $promote;
|
$this->is_admin = $promote;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset user password with a 12 chars random string.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function resetPassword()
|
||||||
|
{
|
||||||
|
$this->password = Hash::make(Str::password(12));
|
||||||
|
|
||||||
|
event(new PasswordReset($this));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the password reset notification.
|
* Send the password reset notification.
|
||||||
*
|
*
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
use App\Api\v1\Controllers\SettingController;
|
use App\Api\v1\Controllers\SettingController;
|
||||||
use App\Api\v1\Controllers\TwoFAccountController;
|
use App\Api\v1\Controllers\TwoFAccountController;
|
||||||
use App\Api\v1\Controllers\UserController;
|
use App\Api\v1\Controllers\UserController;
|
||||||
|
use App\Api\v1\Controllers\UserManagerController;
|
||||||
use Illuminate\Support\Facades\Date;
|
use Illuminate\Support\Facades\Date;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -59,6 +60,11 @@
|
|||||||
* Routes protected by the api authentication guard and restricted to administrators
|
* Routes protected by the api authentication guard and restricted to administrators
|
||||||
*/
|
*/
|
||||||
Route::group(['middleware' => ['auth:api-guard', 'admin']], function () {
|
Route::group(['middleware' => ['auth:api-guard', 'admin']], function () {
|
||||||
|
Route::patch('users/{user}/password/reset', [UserManagerController::class, 'resetPassword'])->name('users.password.reset');
|
||||||
|
Route::delete('users/{user}/pats', [UserManagerController::class, 'revokePATs'])->name('users.revoke.pats');
|
||||||
|
Route::delete('users/{user}/credentials', [UserManagerController::class, 'revokeWebauthnCredentials'])->name('users.revoke.credentials');
|
||||||
|
Route::apiResource('users', UserManagerController::class);
|
||||||
|
|
||||||
Route::get('settings/{settingName}', [SettingController::class, 'show'])->name('settings.show');
|
Route::get('settings/{settingName}', [SettingController::class, 'show'])->name('settings.show');
|
||||||
Route::get('settings', [SettingController::class, 'index'])->name('settings.index');
|
Route::get('settings', [SettingController::class, 'index'])->name('settings.index');
|
||||||
Route::post('settings', [SettingController::class, 'store'])->name('settings.store');
|
Route::post('settings', [SettingController::class, 'store'])->name('settings.store');
|
||||||
|
Loading…
Reference in New Issue
Block a user