Changeset View
Changeset View
Standalone View
Standalone View
src/app/Http/Controllers/API/UsersController.php
<?php | <?php | ||||
namespace App\Http\Controllers\API; | namespace App\Http\Controllers\API; | ||||
use App\Http\Controllers\Controller; | use App\Http\Controllers\Controller; | ||||
use App\Domain; | use App\Domain; | ||||
use App\Rules\UserEmailDomain; | |||||
use App\Rules\UserEmailLocal; | |||||
use App\User; | use App\User; | ||||
use Illuminate\Http\Request; | use Illuminate\Http\Request; | ||||
use Illuminate\Support\Facades\Auth; | use Illuminate\Support\Facades\Auth; | ||||
use Illuminate\Support\Facades\DB; | |||||
use Illuminate\Support\Facades\Validator; | use Illuminate\Support\Facades\Validator; | ||||
use Illuminate\Support\Str; | |||||
class UsersController extends Controller | class UsersController extends Controller | ||||
{ | { | ||||
/** | /** | ||||
* Create a new API\UsersController instance. | * Create a new API\UsersController instance. | ||||
* | * | ||||
* Ensures that the correct authentication middleware is applied except for /login | * Ensures that the correct authentication middleware is applied except for /login | ||||
* | * | ||||
▲ Show 20 Lines • Show All 51 Lines • ▼ Show 20 Lines | class UsersController extends Controller | ||||
/** | /** | ||||
* Get the authenticated User | * Get the authenticated User | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse | * @return \Illuminate\Http\JsonResponse | ||||
*/ | */ | ||||
public function info() | public function info() | ||||
{ | { | ||||
$user = $this->guard()->user(); | $user = $this->guard()->user(); | ||||
$response = $user->toArray(); | $response = $this->userResponse($user); | ||||
// Settings | |||||
// TODO: It might be reasonable to limit the list of settings here to these | |||||
// that are safe and are used in the UI | |||||
$response['settings'] = []; | |||||
foreach ($user->settings as $item) { | |||||
$response['settings'][$item->key] = $item->value; | |||||
} | |||||
// Status info | |||||
$response['statusInfo'] = self::statusInfo($user); | |||||
return response()->json($response); | return response()->json($response); | ||||
} | } | ||||
/** | /** | ||||
* Get a JWT token via given credentials. | * Get a JWT token via given credentials. | ||||
* | * | ||||
* @param \Illuminate\Http\Request $request The API request. | * @param \Illuminate\Http\Request $request The API request. | ||||
Show All 9 Lines | public function login(Request $request) | ||||
'password' => 'required|min:4', | 'password' => 'required|min:4', | ||||
] | ] | ||||
); | ); | ||||
if ($v->fails()) { | if ($v->fails()) { | ||||
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | ||||
} | } | ||||
$credentials = $request->only('email', 'password'); | $credentials = $request->only('email', 'password'); | ||||
if ($token = $this->guard()->attempt($credentials)) { | if ($token = $this->guard()->attempt($credentials)) { | ||||
return $this->respondWithToken($token); | return $this->respondWithToken($token); | ||||
} | } | ||||
return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401); | return response()->json(['status' => 'error', 'message' => __('auth.failed')], 401); | ||||
} | } | ||||
▲ Show 20 Lines • Show All 52 Lines • ▼ Show 20 Lines | class UsersController extends Controller | ||||
{ | { | ||||
if (!$this->hasAccess($id)) { | if (!$this->hasAccess($id)) { | ||||
return $this->errorResponse(403); | return $this->errorResponse(403); | ||||
} | } | ||||
$user = User::find($id); | $user = User::find($id); | ||||
if (empty($user)) { | if (empty($user)) { | ||||
return $this->errorResponse(404); | return $this->errorResponse(404); | ||||
} | } | ||||
return response()->json($user); | $response = $this->userResponse($user); | ||||
return response()->json($response); | |||||
} | } | ||||
/** | /** | ||||
* User status (extended) information | * User status (extended) information | ||||
* | * | ||||
* @param \App\User $user User object | * @param \App\User $user User object | ||||
* | * | ||||
* @return array Status information | * @return array Status information | ||||
Show All 15 Lines | public static function statusInfo(User $user): array | ||||
} elseif ($user->isActive()) { | } elseif ($user->isActive()) { | ||||
$status = 'active'; | $status = 'active'; | ||||
} | } | ||||
list ($local, $domain) = explode('@', $user->email); | list ($local, $domain) = explode('@', $user->email); | ||||
$domain = Domain::where('namespace', $domain)->first(); | $domain = Domain::where('namespace', $domain)->first(); | ||||
// If that is not a public domain, add domain specific steps | // If that is not a public domain, add domain specific steps | ||||
if (!$domain->isPublic()) { | if ($domain && !$domain->isPublic()) { | ||||
$steps['domain-new'] = true; | $steps['domain-new'] = true; | ||||
$steps['domain-ldap-ready'] = 'isLdapReady'; | $steps['domain-ldap-ready'] = 'isLdapReady'; | ||||
$steps['domain-verified'] = 'isVerified'; | $steps['domain-verified'] = 'isVerified'; | ||||
$steps['domain-confirmed'] = 'isConfirmed'; | $steps['domain-confirmed'] = 'isConfirmed'; | ||||
} | } | ||||
// Create a process check list | // Create a process check list | ||||
foreach ($steps as $step_name => $func) { | foreach ($steps as $step_name => $func) { | ||||
Show All 22 Lines | class UsersController extends Controller | ||||
* Create a new user record. | * Create a new user record. | ||||
* | * | ||||
* @param \Illuminate\Http\Request $request The API request. | * @param \Illuminate\Http\Request $request The API request. | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse The response | * @return \Illuminate\Http\JsonResponse The response | ||||
*/ | */ | ||||
public function store(Request $request) | public function store(Request $request) | ||||
{ | { | ||||
// TODO | if ($this->guard()->user()->controller()->id !== $this->guard()->user()->id) { | ||||
return $this->errorResponse(403); | |||||
} | |||||
if ($error_response = $this->validateUserRequest($request, null, $settings)) { | |||||
return $error_response; | |||||
} | |||||
$user_name = !empty($settings['first_name']) ? $settings['first_name'] : ''; | |||||
if (!empty($settings['last_name'])) { | |||||
$user_name .= ' ' . $settings['last_name']; | |||||
} | |||||
DB::beginTransaction(); | |||||
// Create user record | |||||
$user = User::create([ | |||||
'name' => $user_name, | |||||
'email' => $request->email, | |||||
'password' => $request->password, | |||||
]); | |||||
if (!empty($settings)) { | |||||
$user->setSettings($settings); | |||||
} | |||||
// TODO: Assign package | |||||
// Add aliases | |||||
if (!empty($request->aliases)) { | |||||
$user->setAliases($request->aliases); | |||||
} | |||||
DB::commit(); | |||||
return response()->json([ | |||||
'status' => 'success', | |||||
'message' => __('app.user-create-success'), | |||||
]); | |||||
} | } | ||||
/** | /** | ||||
* Update user data. | * Update user data. | ||||
* | * | ||||
* @param \Illuminate\Http\Request $request The API request. | * @param \Illuminate\Http\Request $request The API request. | ||||
* @params string $id User identifier | * @params string $id User identifier | ||||
* | * | ||||
* @return \Illuminate\Http\JsonResponse The response | * @return \Illuminate\Http\JsonResponse The response | ||||
*/ | */ | ||||
public function update(Request $request, $id) | public function update(Request $request, $id) | ||||
{ | { | ||||
if (!$this->hasAccess($id)) { | if (!$this->hasAccess($id)) { | ||||
return $this->errorResponse(403); | return $this->errorResponse(403); | ||||
} | } | ||||
$user = User::find($id); | $user = User::find($id); | ||||
if (empty($user)) { | if (empty($user)) { | ||||
return $this->errorResponse(404); | return $this->errorResponse(404); | ||||
} | } | ||||
$rules = [ | if ($error_response = $this->validateUserRequest($request, $user, $settings)) { | ||||
'external_email' => 'nullable|email', | return $error_response; | ||||
'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/', | |||||
'first_name' => 'string|nullable|max:512', | |||||
'last_name' => 'string|nullable|max:512', | |||||
'billing_address' => 'string|nullable|max:1024', | |||||
'country' => 'string|nullable|alpha|size:2', | |||||
'currency' => 'string|nullable|alpha|size:3', | |||||
]; | |||||
if (!empty($request->password) || !empty($request->password_confirmation)) { | |||||
$rules['password'] = 'required|min:4|max:2048|confirmed'; | |||||
} | |||||
// Validate input | |||||
$v = Validator::make($request->all(), $rules); | |||||
if ($v->fails()) { | |||||
return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); | |||||
} | } | ||||
// Update user settings | DB::beginTransaction(); | ||||
$settings = $request->only(array_keys($rules)); | |||||
unset($settings['password']); | |||||
if (!empty($settings)) { | if (!empty($settings)) { | ||||
$user->setSettings($settings); | $user->setSettings($settings); | ||||
} | } | ||||
// Update user password | // Update user password | ||||
if (!empty($rules['password'])) { | if (!empty($request->password)) { | ||||
$user->password = $request->password; | $user->password = $request->password; | ||||
$user->save(); | $user->save(); | ||||
} | } | ||||
// Update aliases | |||||
if (isset($request->aliases)) { | |||||
$user->setAliases($request->aliases); | |||||
} | |||||
DB::commit(); | |||||
return response()->json([ | return response()->json([ | ||||
'status' => 'success', | 'status' => 'success', | ||||
'message' => __('app.user-update-success'), | 'message' => __('app.user-update-success'), | ||||
]); | ]); | ||||
} | } | ||||
/** | /** | ||||
* Get the guard to be used during authentication. | * Get the guard to be used during authentication. | ||||
Show All 16 Lines | class UsersController extends Controller | ||||
{ | { | ||||
$current_user = $this->guard()->user(); | $current_user = $this->guard()->user(); | ||||
// TODO: Admins, other users | // TODO: Admins, other users | ||||
// FIXME: This probably should be some kind of middleware/guard | // FIXME: This probably should be some kind of middleware/guard | ||||
return $current_user->id == $user_id; | return $current_user->id == $user_id; | ||||
} | } | ||||
/** | |||||
* Create a response data array for specified user. | |||||
* | |||||
* @param \App\User $user User object | |||||
* | |||||
* @return array Response data | |||||
*/ | |||||
protected function userResponse(User $user): array | |||||
{ | |||||
$response = $user->toArray(); | |||||
// Settings | |||||
// TODO: It might be reasonable to limit the list of settings here to these | |||||
// that are safe and are used in the UI | |||||
$response['settings'] = []; | |||||
foreach ($user->settings as $item) { | |||||
$response['settings'][$item->key] = $item->value; | |||||
} | |||||
// Aliases | |||||
$response['aliases'] = []; | |||||
foreach ($user->aliases as $item) { | |||||
$response['aliases'][] = $item->alias; | |||||
} | |||||
// Status info | |||||
$response['statusInfo'] = self::statusInfo($user); | |||||
return $response; | |||||
} | |||||
/** | |||||
* Validate user input | |||||
* | |||||
* @param \Illuminate\Http\Request $request The API request. | |||||
* @param \App\User|null $user User identifier | |||||
* @param array $settings User settings (from the request) | |||||
* | |||||
* @return \Illuminate\Http\JsonResponse The response on error | |||||
*/ | |||||
protected function validateUserRequest(Request $request, $user, &$settings = []) | |||||
{ | |||||
$rules = [ | |||||
'external_email' => 'nullable|email', | |||||
'phone' => 'string|nullable|max:64|regex:/^[0-9+() -]+$/', | |||||
'first_name' => 'string|nullable|max:512', | |||||
'last_name' => 'string|nullable|max:512', | |||||
'billing_address' => 'string|nullable|max:1024', | |||||
'country' => 'string|nullable|alpha|size:2', | |||||
'currency' => 'string|nullable|alpha|size:3', | |||||
'aliases' => 'array|nullable', | |||||
]; | |||||
if (empty($user) || !empty($request->password) || !empty($request->password_confirmation)) { | |||||
$rules['password'] = 'required|min:4|max:2048|confirmed'; | |||||
} | |||||
$errors = []; | |||||
// Validate input | |||||
$v = Validator::make($request->all(), $rules); | |||||
if ($v->fails()) { | |||||
$errors = $v->errors()->toArray(); | |||||
} | |||||
$controller = $user ? $user->controller() : $this->guard()->user(); | |||||
// For new user validate email address | |||||
if (empty($user)) { | |||||
$email = $request->email; | |||||
if (empty($email)) { | |||||
$errors['email'] = \trans('validation.required', ['attribute' => 'email']); | |||||
} elseif ($error = self::validateEmail($email, $controller, false)) { | |||||
$errors['email'] = $error; | |||||
} | |||||
} | |||||
// Validate aliases input | |||||
if (isset($request->aliases)) { | |||||
$aliases = []; | |||||
$existing_aliases = $user ? $user->aliases()->get()->pluck('alias')->toArray() : []; | |||||
foreach ($request->aliases as $idx => $alias) { | |||||
if (is_string($alias) && !empty($alias)) { | |||||
// Alias cannot be the same as the email address (new user) | |||||
if (!empty($email) && Str::lower($alias) == Str::lower($email)) { | |||||
continue; | |||||
} | |||||
// validate new aliases | |||||
if ( | |||||
!in_array($alias, $existing_aliases) | |||||
&& ($error = self::validateEmail($alias, $controller, true)) | |||||
) { | |||||
if (!isset($errors['aliases'])) { | |||||
$errors['aliases'] = []; | |||||
} | |||||
$errors['aliases'][$idx] = $error; | |||||
continue; | |||||
} | |||||
$aliases[] = $alias; | |||||
} | |||||
} | |||||
$request->aliases = $aliases; | |||||
} | |||||
if (!empty($errors)) { | |||||
return response()->json(['status' => 'error', 'errors' => $errors], 422); | |||||
} | |||||
// Update user settings | |||||
$settings = $request->only(array_keys($rules)); | |||||
unset($settings['password'], $settings['aliases'], $settings['email']); | |||||
} | |||||
/** | |||||
* Email address (login or alias) validation | |||||
* | |||||
* @param string $email Email address | |||||
* @param \App\User $user The account owner | |||||
* @param bool $is_alias The email is an alias | |||||
* | |||||
* @return string Error message on validation error | |||||
*/ | |||||
protected static function validateEmail(string $email, User $user, bool $is_alias = false): ?string | |||||
{ | |||||
$attribute = $is_alias ? 'alias' : 'email'; | |||||
if (strpos($email, '@') === false) { | |||||
return \trans('validation.entryinvalid', ['attribute' => $attribute]); | |||||
} | |||||
list($login, $domain) = explode('@', $email); | |||||
// Check if domain exists | |||||
$domain = Domain::where('namespace', Str::lower($domain))->first(); | |||||
if (empty($domain)) { | |||||
return \trans('validation.domaininvalid'); | |||||
} | |||||
// Validate login part alone | |||||
$v = Validator::make( | |||||
[$attribute => $login], | |||||
[$attribute => ['required', new UserEmailLocal(!$domain->isPublic())]] | |||||
); | |||||
if ($v->fails()) { | |||||
return $v->errors()->toArray()[$attribute][0]; | |||||
} | |||||
// Check if it is one of domains available to the user | |||||
// TODO: We should have a helper that returns "flat" array with domain names | |||||
// I guess we could use pluck() somehow | |||||
$domains = array_map( | |||||
function ($domain) { | |||||
return $domain->namespace; | |||||
}, | |||||
$user->domains() | |||||
); | |||||
if (!in_array($domain->namespace, $domains)) { | |||||
return \trans('validation.entryexists', ['attribute' => 'domain']); | |||||
} | |||||
// Check if user with specified address already exists | |||||
if (User::findByEmail($email)) { | |||||
return \trans('validation.entryexists', ['attribute' => $attribute]); | |||||
} | |||||
return null; | |||||
} | |||||
} | } |