File: /home/xedaptot/be.naniguide.com/app/Model/User.php
<?php
/**
* User class.
*
* Model class for user
*
* LICENSE: This product includes software developed at
* the Acelle Co., Ltd. (http://acellemail.com/).
*
* @category MVC Model
*
* @author N. Pham <[email protected]>
* @author L. Pham <[email protected]>
* @copyright Acelle Co., Ltd
* @license Acelle Co., Ltd
*
* @version 1.0
*
* @link http://acellemail.com
*/
namespace Acelle\Model;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Acelle\Notifications\ResetPassword;
use Acelle\Library\ExtendedSwiftMessage;
use Acelle\Model\Setting;
use Illuminate\Http\UploadedFile;
use App;
use AppUrl;
use Acelle\Library\Tool;
use Acelle\Library\Traits\HasUid;
use Acelle\Library\Facades\Hook;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Acelle\Library\Traits\HasSettingsField;
class User extends Authenticatable
{
use Notifiable;
use HasSettingsField;
use HasUid;
protected $connection = 'mysql';
/*******
* Important: Storage location: User vs Customer
* One customer (account) may have one or more users
*
* User has its own location for storing:
* + Lock files
* + Assets (which is used by File Manager)
*
* Customer has a common place to store account's assets (available to all users of the same account)
* + Templates
* + Attachments (account-wide, not individual user)
*
*/
public const BASE_DIR = 'app/users';
public const ASSETS_DIR = 'home/files';
public const ASSETS_THUMB_DIR = 'home/thumbs';
public const PROFILE_IMAGE_PATH = 'home/avatar';
public const PROFILE_THUMB_PATH = 'home/avatar-thumb';
public const STATUS_ACTIVE = 'active';
public const STATUS_INACTIVE = 'inactive';
// Role
public const ROLE_FULL_ACCESS = 'full-access';
public const ROLE_READ_ONLY = 'read-only';
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'email', 'first_name', 'last_name', 'phone'
];
/**
* The attributes that should be hidden for arrays.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
/**
* Associations.
*
* @var object | collect
*/
public function customer()
{
return $this->belongsTo('Acelle\Model\Customer');
}
public function admin()
{
return $this->hasOne('Acelle\Model\Admin');
}
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'user_role');
}
public function getRole()
{
if ($this->roles()->count() > 1) {
throw new \Exception('User can not have more than one role!');
}
return $this->roles()->first();
}
public function createUserDirectories()
{
$paths = [
$this->getBasePath(),
$this->getAssetsPath(),
$this->getThumbsPath(),
];
foreach ($paths as $path) {
if (!\Illuminate\Support\Facades\File::exists($path)) {
\Illuminate\Support\Facades\File::makeDirectory($path, 0777, true, true);
}
}
}
/**
* Get authenticate from file.
*
* @return string
*/
public static function getAuthenticateFromFile()
{
$path = base_path('.authenticate');
if (!isSiteDemo()) {
return ['email' => '', 'password' => ''];
}
if (file_exists($path)) {
$content = \Illuminate\Support\Facades\File::get($path);
$lines = explode("\n", $content);
if (count($lines) > 1) {
$demo = session()->get('demo');
if (!isset($demo) || $demo == 'backend') {
return ['email' => $lines[0], 'password' => $lines[1]];
} else {
return ['email' => $lines[2], 'password' => $lines[3]];
}
}
}
return ['email' => '', 'password' => ''];
}
/**
* Send regitration activation email.
*
* @return string
*/
public function sendActivationMail($name = null, $planUid = null)
{
$layout = \Acelle\Model\Layout::findByAlias('registration_confirmation_email');
$token = $this->getToken();
$layout->content = str_replace('{ACTIVATION_URL}', join_url(config('app.url'), action('UserController@activate', ['token' => $token, 'plan_uid' => $planUid], false)), $layout->content);
$layout->content = str_replace('{CUSTOMER_NAME}', $name, $layout->content);
$name = is_null($name) ? trans('messages.to_email_name') : $name;
// build the message
$message = new ExtendedSwiftMessage();
$message->setEncoder(new \Swift_Mime_ContentEncoder_PlainContentEncoder('8bit'));
$message->setContentType('text/html; charset=utf-8');
$message->setSubject($layout->subject);
$message->setTo([$this->email => $name]);
$message->setReplyTo(Setting::get('mail.reply_to'));
$message->addPart($layout->content, 'text/html');
$mailer = App::make('xmailer');
$result = $mailer->sendWithDefaultFromAddress($message);
if (!empty(Setting::get('notification.on_user_created'))) {
$message->setTo(Setting::get('notification.on_user_created'));
$mailer->sendWithDefaultFromAddress($message);
echo "OK";
}
if (array_key_exists('error', $result)) {
throw new \Exception($result['error']);
}
}
/**
* The rules for validation.
*
* @var array
*/
public function rules()
{
$rules = array(
'email' => 'required|email|unique:users,email,'.$this->id.',id',
'first_name' => 'required',
'last_name' => 'required',
'timezone' => 'required',
'language_id' => 'required',
'image' => 'nullable|image',
'phone' => [(Setting::get('user.require_mobile_phone') == 'yes' ? 'required' : 'nullable'), 'regex:/^[\+0-9]{10,15}$/'],
);
if (isset($this->id)) {
$rules['password'] = 'nullable|confirmed|min:5|max:255';
} else {
$rules['password'] = 'required|confirmed|min:5|max:255';
}
return $rules;
}
/**
* The rules for validation.
*
* @var array
*/
public function registerRules()
{
$rules = array(
'email' => 'required|email|unique:users,email,'.$this->id.',id',
'first_name' => 'required',
'last_name' => 'required',
'timezone' => 'required',
'language_id' => 'required',
'phone' => [(Setting::get('user.require_mobile_phone') == 'yes' ? 'required' : 'nullable'), 'regex:/^[\+0-9]{10,15}$/'],
);
if (isset($this->id)) {
$rules['password'] = 'min:5|max:255';
} else {
$rules['password'] = 'required|max:255';
}
return $rules;
}
/**
* The rules for validation via api.
*
* @var array
*/
public function apiRules()
{
return array(
'email' => 'required|email|unique:users,email,'.$this->id.',id',
'first_name' => 'required',
'last_name' => 'required',
'timezone' => 'required',
'language_id' => 'required',
'password' => 'required|min:5',
'phone' => [(Setting::get('user.require_mobile_phone') == 'yes' ? 'required' : 'nullable'), 'regex:/^[\+0-9]{10,15}$/'],
);
}
/**
* The rules for validation via api.
*
* @var array
*/
public function apiUpdateRules($request)
{
$arr = [];
if (isset($request->email)) {
$arr['email'] = 'required|email|unique:users,email,'.$this->id.',id';
}
if (isset($request->first_name)) {
$arr['first_name'] = 'required';
}
if (isset($request->last_name)) {
$arr['last_name'] = 'required';
}
if (isset($request->timezone)) {
$arr['timezone'] = 'required';
}
if (isset($request->language_id)) {
$arr['language_id'] = 'required';
}
if (isset($request->password)) {
$arr['password'] = 'min:5|max:255';
}
return $arr;
}
/**
* User activation.
*
* @return string
*/
public function userActivation()
{
return $this->hasOne('Acelle\Model\userActivation');
}
/**
* Create activation token for user.
*
* @return string
*/
public function getToken()
{
$token = \Acelle\Model\UserActivation::getToken();
$userActivation = $this->userActivation;
if (!$userActivation) {
$userActivation = new \Acelle\Model\UserActivation();
$userActivation->user_id = $this->id;
}
$userActivation->token = $token;
$userActivation->save();
return $token;
}
/**
* Set user is activated.
*
* @return bool
*/
public function setActivated()
{
$this->activated = true;
$this->save();
}
/**
* Bootstrap any application services.
*/
public static function boot()
{
parent::boot();
// Create uid when creating list.
static::creating(function ($item) {
// Create new uid
$uid = uniqid();
$item->uid = $uid;
// Add api token
$item->api_token = str_random(60);
});
}
/**
* Check if user has admin account.
*/
public function isAdmin()
{
return !is_null($this->admin);
}
/**
* Send the password reset notification.
*
* @param string $token
*/
public function sendPasswordResetNotification($token)
{
// $this->notify(new ResetPassword($token, url('password/reset', $token)));
$resetPasswordUrl = url('password/reset', $token);
$htmlContent = '<p>Please click the link below to reset your password:<br><a href="'.$resetPasswordUrl.'">'.$resetPasswordUrl.'</a>';
// build the message
$message = new ExtendedSwiftMessage();
$message->setEncoder(new \Swift_Mime_ContentEncoder_PlainContentEncoder('8bit'));
$message->setContentType('text/html; charset=utf-8');
$message->setSubject('Password Reset');
$message->setTo($this->email);
$message->setReplyTo(Setting::get('mail.reply_to'));
$message->addPart($htmlContent, 'text/html');
$mailer = App::make('xmailer');
$result = $mailer->sendWithDefaultFromAddress($message);
if (array_key_exists('error', $result)) {
throw new \Exception($result['error']);
}
}
public function getLockPath($path)
{
$base = $this->getBasePath('locks');
if (!\Illuminate\Support\Facades\File::exists($base)) {
\Illuminate\Support\Facades\File::makeDirectory($base, 0777, true, true);
}
return join_paths($base, $path);
}
public function getAssetsPath($path = null)
{
$base = $this->getBasePath(self::ASSETS_DIR);
if (!\Illuminate\Support\Facades\File::exists($base)) {
\Illuminate\Support\Facades\File::makeDirectory($base, 0777, true, true);
}
return join_paths($base, $path);
}
public function getThumbsPath($path = null)
{
$base = $this->getBasePath(self::ASSETS_THUMB_DIR);
if (!\Illuminate\Support\Facades\File::exists($base)) {
\Illuminate\Support\Facades\File::makeDirectory($base, 0777, true, true);
}
return join_paths($base, $path);
}
public function getBasePath($path = null)
{
$base = storage_path(join_paths(self::BASE_DIR, $this->uid));
if (!\Illuminate\Support\Facades\File::exists($base)) {
\Illuminate\Support\Facades\File::makeDirectory($base, 0777, true, true);
}
return join_paths($base, $path);
}
/**
* Generate one time token.
*/
public function generateOneTimeToken()
{
$this->one_time_api_token = generateRandomString(32);
$this->save();
}
/**
* Clear one time token.
*/
public function clearOneTimeToken()
{
$this->one_time_api_token = null;
$this->save();
}
public function getAssetsSubUrl()
{
$appSubDirectory = \Acelle\Helpers\getAppSubdirectory();
$appSubDirectory = (is_null($appSubDirectory)) ? '' : $appSubDirectory;
// Returns a relative subpath like "/files/000000"
$subpath = route('user_files', ['uid' => $this->uid], false);
return join_paths($appSubDirectory, $subpath);
}
public function getThumbsSubUrl()
{
$appSubDirectory = \Acelle\Helpers\getAppSubdirectory();
$appSubDirectory = (is_null($appSubDirectory)) ? '' : $appSubDirectory;
// Returns a relative subpath like "/thumbs/000000"
$subpath = route('user_thumbs', ['uid' => $this->uid], false) ;
return join_paths($appSubDirectory, $subpath);
}
public function getFilemanagerConfig($isAdmin = false)
{
// Create user directories if not exists
$this->createUserDirectories();
// Base URL with port, but without subpath
$baseUrl = parse_url(url('/'));
// port may be null
if (!array_key_exists('port', $baseUrl)) {
$baseUrl['port'] = '';
}
$baseUrl = "{$baseUrl['scheme']}://{$baseUrl['host']}{$baseUrl['port']}";
// Limitation
// Since this method is called from within the dialog.php file, there is no way to know if whether or not it is an admin or customer session
// So, there is no chance to apply quota here
if ($isAdmin) {
// Virtually unlimited
$maxSizeTotal = 2048;
$maxSizeUpload = 2048;
} else {
// @todo this is a temporary fix
$maxSizeTotal = 2048;
$maxSizeUpload = 2048;
/* The correct way to handle it is as below
* However, in certain cases, the $isAdmin param cannot be passed correctly from filemanager
* (we have to modify the filemanager source)
*
* /
/*
$maxSizeTotal = get_tmp_quota($this->customer, "max_size_upload_total") > 0 ? get_tmp_quota($this->customer, "max_size_upload_total") : 2048;
$maxSizeUpload = get_tmp_quota($this->customer, "max_file_size_upload") > 0 ? get_tmp_quota($this->customer, "max_file_size_upload") : 2048;
*/
}
$config = [
'base_url' => $baseUrl,
// The `upload_dir` is needed to make the final file URL which is compatible with `user_files` route
// For example: "/files/000000/example.jpg"
'upload_dir' => join_paths('/', $this->getAssetsSubUrl(), '/'),
'thumb_dir' => join_paths('/', $this->getThumbsSubUrl(), '/'),
'thumbs_upload_dir' => join_paths('/', $this->getThumbsSubUrl(), '/'),
// relative path from filemanager folder to upload folder, WITH FINAL /
'current_path' => join_paths('../../storage/', self::BASE_DIR, $this->uid, self::ASSETS_DIR, '/'),
// relative path from filemanager folder to upload folder, WITH FINAL /
'thumbs_base_path' => join_paths('../../storage/', self::BASE_DIR, $this->uid, self::ASSETS_THUMB_DIR, '/'),
'MaxSizeTotal' => $maxSizeTotal,
'MaxSizeUpload' => $maxSizeUpload,
];
return $config;
}
public function getProfileImageUrl()
{
$path = $this->getProfileImagePath();
if (file_exists($path)) {
return \Acelle\Helpers\generatePublicPath($path) . '?v=' . md5_file($this->getProfileImagePath());
} else {
return AppUrl::asset('images/user-placeholder.svg');
}
}
public function getProfileThumbUrl()
{
$path = $this->getProfileThumbPath();
if (file_exists($path)) {
return \Acelle\Helpers\generatePublicPath($path);
} else {
return AppUrl::asset('images/user-placeholder.svg');
}
}
public function getProfileImagePath()
{
return $this->getBasePath(self::PROFILE_IMAGE_PATH);
}
public function getProfileThumbPath()
{
return $this->getBasePath(self::PROFILE_THUMB_PATH);
}
/**
* Upload and resize avatar.
*
* @var void
*/
public function uploadProfileImage(UploadedFile $file)
{
// Full path: /storage/app/users/000000/home/avatar
$path = $this->getProfileImagePath();
// File name: avatar
$filename = basename($path);
// The base dir: /storage/app/users/000000/home/
$dirname = dirname($path);
// save to server at /storage/app/users/000000/home/avatar
$file->move($dirname, $filename);
// create thumbnails and replace the original image with the the small-sized thumbnail
$img = \Image::make($path);
$img->fit(120, 120)->save($path);
return $path;
}
public function removeProfileImage()
{
$path = $this->getProfileImagePath();
if (file_exists($path)) {
\Illuminate\Support\Facades\File::delete($path);
}
$thumb = $this->getProfileThumbPath();
if (file_exists($thumb)) {
\Illuminate\Support\Facades\File::delete($thumb);
}
}
public function deleteAndCleanup()
{
// User's storage location
// For example: storage/app/users/000000/
$path = $this->getBasePath();
if (file_exists($path)) {
Tool::xdelete($path);
}
$this->delete();
}
public function generateOneClickLoginUrl()
{
return action('AppController@autoLogin', [
'api_token' => $this->api_token,
]);
}
public static function scopeSearch($query, $keyword)
{
$query = $query->select('users.*');
// Keyword
if (!empty(trim($keyword))) {
foreach (explode(' ', trim($keyword)) as $keyword) {
$query = $query->where(function ($q) use ($keyword) {
$q->orwhere('users.first_name', 'like', '%'.$keyword.'%')
->orWhere('users.last_name', 'like', '%'.$keyword.'%')
->orWhere('users.email', 'like', '%'.$keyword.'%');
});
}
}
}
public function displayName($lastNameFirst)
{
if ($lastNameFirst) {
return htmlspecialchars(trim($this->last_name.' '.$this->first_name));
} else {
return htmlspecialchars(trim($this->first_name.' '.$this->last_name));
}
}
public function isActivated()
{
return $this->activated;
}
public static function newDefault()
{
$user = new self();
$user->activated = true;
$user->status = self::STATUS_ACTIVE;
return $user;
}
public function fillAttributes($params)
{
$this->fill($params);
}
public function enable()
{
$this->activated = 1;
$this->save();
}
public function disable()
{
$this->activated = 0;
$this->save();
}
// @todo remove this later
public function isReadOnly()
{
return false;
}
public function setRole($role)
{
// for now, one user only has one role
// the same role, do nothing
if ($this->getRole() && $this->getRole()->id == $role->id) {
return;
}
// remove current role if it exists
if ($this->getRole()) {
$this->roles()->detach($this->getRole());
}
// set new role
$this->roles()->save($role);
}
public function hasPermission($permission)
{
// default is access all if has not role
if (!$this->getRole()) {
return false;
}
return $this->getRole()->hasPermission($permission);
}
public function getOnscreenIntros()
{
if (!$this->onscreen_intros) {
return [];
}
return json_decode($this->onscreen_intros, true);
}
public function getOnscreenIntro($screenName)
{
$screenIntros = $this->getOnscreenIntros();
if (!isset($screenIntros[$screenName])) {
return null;
}
return $screenIntros[$screenName];
}
public function setOnscreenIntro($screenName, $data)
{
$screenIntros = $this->getOnscreenIntros();
$screenIntros[$screenName] = $data;
$this->onscreen_intros = json_encode($screenIntros);
$this->save();
}
public function isOnscreenIntroShowed($screenName)
{
$screenIntro = $this->getOnscreenIntro($screenName);
if (!$screenIntro) {
return false;
}
return $screenIntro['showed'];
}
public function setOnscreenIntroShowed($screenName)
{
$screenIntro = $this->getOnscreenIntro($screenName);
if (!$screenIntro) {
$screenIntro = [
'showed' => true,
];
} else {
$screenIntro['showed'] = true;
}
$this->setOnscreenIntro($screenName, $screenIntro);
}
public function generateEmailVerifyCode()
{
$numbers = str_shuffle('0123456789');
$randomString = substr($numbers, 0, 6);
$this->two_factor_verify_code = $randomString;
$this->save();
return $this->two_factor_verify_code;
}
public function getLastSendUntilNowInSeconds()
{
return $this->two_factor_last_send ? \Carbon\Carbon::now()->diffInSeconds($this->two_factor_last_send, $abs = true) : 600;
}
public function getRemainVerifyCountdown()
{
return (60 > $this->getLastSendUntilNowInSeconds()) ? (60 - $this->getLastSendUntilNowInSeconds()) : 0;
}
public function sendVerifyCodeEMail()
{
// resend in 60 seconds
if ($this->getLastSendUntilNowInSeconds() < 60) {
throw new \Exception('Last message sent less than 60 seconds ago. Cannot send messages too quickly.');
}
// code
$code = $this->generateEmailVerifyCode();
// build the message
$message = new ExtendedSwiftMessage();
$message->setEncoder(new \Swift_Mime_ContentEncoder_PlainContentEncoder('8bit'));
$message->setContentType('text/html; charset=utf-8');
$message->setSubject(trans('messages.2fa.verification_code'));
$message->setTo($this->email);
$message->setReplyTo(Setting::get('mail.reply_to'));
$message->addPart(view('auth.2fa.email.content', [
'code' => $code,
])->render(), 'text/html');
$mailer = App::make('xmailer');
$result = $mailer->sendWithDefaultFromAddress($message);
if (array_key_exists('error', $result)) {
throw new \Exception($result['error']);
}
// \Illuminate\Support\Facades\Mail::to($this)->send(new \Acelle\Mail\VerifyEmailSendCode($code));
//
$this->two_factor_last_send = \Carbon\Carbon::now()->timezone('UTC');
$this->save();
}
public function verifyCode($inputCode)
{
$sucess = $this->two_factor_verify_code == trim($inputCode);
if ($sucess) {
// session update
request()->session()->put('two_factor_authenticated', true);
}
return $sucess;
}
public function is2FAAuthenticated()
{
return request()->session()->has('two_factor_authenticated');
}
public function set2FAAuthenticated()
{
// session update
request()->session()->put('two_factor_authenticated', true);
}
public function getInlineGAImage()
{
return \Google2FA::getQRCodeInline(
\Acelle\Model\Setting::get('site_name'),
$this->email,
$this->google_2fa_secret_key,
);
}
public function is2FAEnabled()
{
return $this->enable_2fa && ($this->is2FAEmailEnabled() || $this->is2FAGoogleAuthentictorEnabled());
}
public function is2FAEmailEnabled()
{
return $this->enable_2fa_email;
}
public function is2FAGoogleAuthentictorEnabled()
{
return $this->enable_2fa_google_authenticator && $this->google_2fa_secret_key;
}
public function saveUser(
$email,
$password,
$passwordConfirmation,
$first_name,
$last_name,
$image,
$role_uid,
$phone = null
) {
$rules = [
'email' => 'required|email|unique:users,email,'.$this->id.',id',
'first_name' => 'required',
'last_name' => 'required',
'image' => 'nullable|image',
'role_uid' => 'required',
'phone' => [(Setting::get('user.require_mobile_phone') == 'yes' ? 'required' : 'nullable'), 'regex:/^[\+0-9]{10,15}$/'],
];
if (isset($this->id)) {
$rules['password'] = 'nullable|confirmed|min:5|max:255';
} else {
$rules['password'] = 'required|confirmed|min:5|max:255';
}
// fill
$this->email = $email;
if ($password) {
$this->password = $password;
}
$this->first_name = $first_name;
$this->last_name = $last_name;
$this->phone = $phone;
$validator = \Validator::make([
'email' => $email,
'password' => $password,
'password_confirmation' => $passwordConfirmation,
'first_name' => $first_name,
'last_name' => $last_name,
'image' => $image,
'role_uid' => $role_uid,
'phone' => $phone,
], $rules);
// validate
if ($validator->fails()) {
return [false, $validator->errors()];
}
// find role
$role = Role::findByUid($role_uid);
// Update password
if (!empty($password)) {
$this->password = bcrypt($password);
}
// is_new check
$isNew = !$this->id;
// save
$this->save();
// Hook
if ($isNew) {
Hook::execute('user_added', [$this]);
}
// Upload and save image
if ($image) {
if ($image->isValid()) {
// Remove old images
$this->uploadProfileImage($image);
}
}
// role is required
$this->setRole($role);
return [true, $validator->errors()];
}
}