diff --git a/src/app/Console/Commands/Status/Health.php b/src/app/Console/Commands/Status/Health.php index f9d82889..758693b3 100644 --- a/src/app/Console/Commands/Status/Health.php +++ b/src/app/Console/Commands/Status/Health.php @@ -1,198 +1,201 @@ line($exception); return false; } } private function checkOpenExchangeRates() { try { OpenExchangeRates::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkMollie() { try { return Mollie::healthcheck(); } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkDAV() { try { DAV::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkLDAP() { try { LDAP::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkIMAP() { try { IMAP::healthcheck(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkRoundcube() { try { //TODO maybe run a select? Roundcube::dbh(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkRedis() { try { Redis::connection(); return true; } catch (\Exception $exception) { $this->line($exception); return false; } } private function checkMeet() { $urls = \config('meet.api_urls'); $success = true; + foreach ($urls as $url) { $this->line("Checking $url"); try { $client = new \GuzzleHttp\Client( [ 'http_errors' => false, // No exceptions from Guzzle 'base_uri' => $url, 'verify' => \config('meet.api_verify_tls'), 'headers' => [ 'X-Auth-Token' => \config('meet.api_token'), ], 'connect_timeout' => 10, 'timeout' => 10, 'on_stats' => function (\GuzzleHttp\TransferStats $stats) { $threshold = \config('logging.slow_log'); if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) { $url = $stats->getEffectiveUri(); $method = $stats->getRequest()->getMethod(); \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec)); } }, ] ); $response = $client->request('GET', "ping"); if ($response->getStatusCode() != 200) { - $this->line("Backend {$url} not available. Status: {$response->getStatusCode()} Reason: {$response->getReasonPhrase()}"); + $code = $response->getStatusCode(); + $reason = $response->getReasonPhrase(); $success = false; + $this->line("Backend {$url} not available. Status: {$code} Reason: {$reason}"); } } catch (\Exception $exception) { - $this->line("Backend {$url} not available:"); - $this->line($exception); $success = false; + $this->line("Backend {$url} not available. Error: {$exception}"); } } return $success; } /** * Execute the console command. * * @return mixed */ public function handle() { $result = 0; $steps = $this->option('check'); if (empty($steps)) { $steps = [ 'DB', 'Redis', 'IMAP', 'Roundcube', 'Meet', 'DAV', 'Mollie', 'OpenExchangeRates', ]; if (\config('app.with_ldap')) { array_unshift($steps, 'LDAP'); } } foreach ($steps as $step) { $func = "check{$step}"; $this->line("Checking {$step}..."); if ($this->{$func}()) { $this->info("OK"); } else { $this->error("Not found"); $result = 1; } } return $result; } } diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php index 49a89d18..b1abca6b 100644 --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -1,616 +1,617 @@ orderBy('months')->orderByDesc('title')->get() ->map(function ($plan) { $button = self::trans("app.planbutton-{$plan->title}"); if (strpos($button, 'app.planbutton') !== false) { $button = self::trans('app.planbutton', ['plan' => $plan->name]); } return [ 'title' => $plan->title, 'name' => $plan->name, 'button' => $button, 'description' => $plan->description, 'mode' => $plan->mode ?: Plan::MODE_EMAIL, 'isDomain' => $plan->hasDomain(), ]; }) ->all(); return response()->json(['status' => 'success', 'plans' => $plans]); } /** * Returns list of public domains for signup. * * @param \Illuminate\Http\Request $request HTTP request * * @return \Illuminate\Http\JsonResponse JSON response */ public function domains(Request $request) { return response()->json(['status' => 'success', 'domains' => Domain::getPublicDomains()]); } /** * Starts signup process. * * Verifies user name and email/phone, sends verification email/sms message. * Returns the verification code. * * @param \Illuminate\Http\Request $request HTTP request * * @return \Illuminate\Http\JsonResponse JSON response */ public function init(Request $request) { $rules = [ 'first_name' => 'max:128', 'last_name' => 'max:128', 'voucher' => 'max:32', ]; $plan = $this->getPlan(); if ($plan->mode == Plan::MODE_TOKEN) { $rules['token'] = ['required', 'string', new SignupToken()]; } else { $rules['email'] = ['required', 'string', new SignupExternalEmail()]; } // Check required fields, validate input $v = Validator::make($request->all(), $rules); if ($v->fails()) { return response()->json(['status' => 'error', 'errors' => $v->errors()->toArray()], 422); } // Generate the verification code $code = SignupCode::create([ 'email' => $plan->mode == Plan::MODE_TOKEN ? $request->token : $request->email, 'first_name' => $request->first_name, 'last_name' => $request->last_name, 'plan' => $plan->title, 'voucher' => $request->voucher, ]); $response = [ 'status' => 'success', 'code' => $code->code, 'mode' => $plan->mode ?: 'email', ]; if ($plan->mode == Plan::MODE_TOKEN) { // Token verification, jump to the last step $has_domain = $plan->hasDomain(); $response['short_code'] = $code->short_code; $response['is_domain'] = $has_domain; $response['domains'] = $has_domain ? [] : Domain::getPublicDomains(); } else { // External email verification, send an email message SignupVerificationEmail::dispatch($code); } return response()->json($response); } /** * Returns signup invitation information. * * @param string $id Signup invitation identifier * * @return \Illuminate\Http\JsonResponse|void */ public function invitation($id) { $invitation = SignupInvitation::withEnvTenantContext()->find($id); if (empty($invitation) || $invitation->isCompleted()) { return $this->errorResponse(404); } $has_domain = $this->getPlan()->hasDomain(); $result = [ 'id' => $id, 'is_domain' => $has_domain, 'domains' => $has_domain ? [] : Domain::getPublicDomains(), ]; return response()->json($result); } /** * Validation of the verification code. * * @param \Illuminate\Http\Request $request HTTP request * @param bool $update Update the signup code record * * @return \Illuminate\Http\JsonResponse JSON response */ public function verify(Request $request, $update = true) { // Validate the request args $v = Validator::make( $request->all(), [ 'code' => 'required', 'short_code' => 'required', ] ); if ($v->fails()) { return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } // Validate the verification code $code = SignupCode::find($request->code); if ( empty($code) || $code->isExpired() || Str::upper($request->short_code) !== Str::upper($code->short_code) ) { $errors = ['short_code' => "The code is invalid or expired."]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } // For signup last-step mode remember the code object, so we can delete it // with single SQL query (->delete()) instead of two $request->code = $code; if ($update) { $code->verify_ip_address = $request->ip(); $code->save(); } $has_domain = $this->getPlan()->hasDomain(); // Return user name and email/phone/voucher from the codes database, // domains list for selection and "plan type" flag return response()->json([ 'status' => 'success', 'email' => $code->email, 'first_name' => $code->first_name, 'last_name' => $code->last_name, 'voucher' => $code->voucher, 'is_domain' => $has_domain, 'domains' => $has_domain ? [] : Domain::getPublicDomains(), ]); } /** * Validates the input to the final signup request. * * @param \Illuminate\Http\Request $request HTTP request * * @return \Illuminate\Http\JsonResponse JSON response */ public function signupValidate(Request $request) { // Validate input $v = Validator::make( $request->all(), [ 'login' => 'required|min:2', 'password' => ['required', 'confirmed', new Password()], 'domain' => 'required', 'voucher' => 'max:32', ] ); if ($v->fails()) { return response()->json(['status' => 'error', 'errors' => $v->errors()], 422); } $settings = []; // Plan parameter is required/allowed in mandate mode if (!empty($request->plan) && empty($request->code) && empty($request->invitation)) { $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first(); if (!$plan || $plan->mode != Plan::MODE_MANDATE) { $msg = self::trans('validation.exists', ['attribute' => 'plan']); return response()->json(['status' => 'error', 'errors' => ['plan' => $msg]], 422); } } elseif ($request->invitation) { // Signup via invitation $invitation = SignupInvitation::withEnvTenantContext()->find($request->invitation); if (empty($invitation) || $invitation->isCompleted()) { return $this->errorResponse(404); } // Check required fields $v = Validator::make( $request->all(), [ 'first_name' => 'max:128', 'last_name' => 'max:128', ] ); $errors = $v->fails() ? $v->errors()->toArray() : []; if (!empty($errors)) { return response()->json(['status' => 'error', 'errors' => $errors], 422); } $settings = [ 'external_email' => $invitation->email, 'first_name' => $request->first_name, 'last_name' => $request->last_name, ]; } else { // Validate verification codes (again) $v = $this->verify($request, false); if ($v->status() !== 200) { return $v; } $plan = $this->getPlan(); // Get user name/email from the verification code database $code_data = $v->getData(); $settings = [ 'first_name' => $code_data->first_name, 'last_name' => $code_data->last_name, ]; if ($plan->mode == Plan::MODE_TOKEN) { $settings['signup_token'] = $code_data->email; } else { $settings['external_email'] = $code_data->email; } } // Find the voucher discount if ($request->voucher) { $discount = Discount::where('code', \strtoupper($request->voucher)) ->where('active', true)->first(); if (!$discount) { $errors = ['voucher' => self::trans('validation.voucherinvalid')]; return response()->json(['status' => 'error', 'errors' => $errors], 422); } } if (empty($plan)) { $plan = $this->getPlan(); } $is_domain = $plan->hasDomain(); // Validate login if ($errors = self::validateLogin($request->login, $request->domain, $is_domain)) { return response()->json(['status' => 'error', 'errors' => $errors], 422); } // Set some properties for signup() method $request->settings = $settings; $request->plan = $plan; $request->discount = $discount ?? null; $request->invitation = $invitation ?? null; $result = []; if ($plan->mode == Plan::MODE_MANDATE) { $result = $this->mandateForPlan($plan, $request->discount); } return response()->json($result + ['status' => 'success']); } /** * Finishes the signup process by creating the user account. * * @param \Illuminate\Http\Request $request HTTP request * * @return \Illuminate\Http\JsonResponse JSON response */ public function signup(Request $request) { $v = $this->signupValidate($request); if ($v->status() !== 200) { return $v; } $is_domain = $request->plan->hasDomain(); // We allow only ASCII, so we can safely lower-case the email address $login = Str::lower($request->login); $domain_name = Str::lower($request->domain); $domain = null; $user_status = User::STATUS_RESTRICTED; - if ($request->discount && $request->discount->discount == 100 + if ( + $request->discount && $request->discount->discount == 100 && $request->plan->mode == Plan::MODE_MANDATE ) { $user_status = User::STATUS_ACTIVE; } DB::beginTransaction(); // Create domain record if ($is_domain) { $domain = Domain::create([ 'namespace' => $domain_name, 'type' => Domain::TYPE_EXTERNAL, ]); } // Create user record $user = User::create([ 'email' => $login . '@' . $domain_name, 'password' => $request->password, 'status' => $user_status, ]); if ($request->discount) { $wallet = $user->wallets()->first(); $wallet->discount()->associate($request->discount); $wallet->save(); } $user->assignPlan($request->plan, $domain); // Save the external email and plan in user settings $user->setSettings($request->settings); // Update the invitation if ($request->invitation) { $request->invitation->status = SignupInvitation::STATUS_COMPLETED; $request->invitation->user_id = $user->id; $request->invitation->save(); } // Soft-delete the verification code, and store some more info with it if ($request->code) { $request->code->user_id = $user->id; $request->code->submit_ip_address = $request->ip(); $request->code->deleted_at = \now(); $request->code->timestamps = false; $request->code->save(); } DB::commit(); $response = AuthController::logonResponse($user, $request->password); if ($request->plan->mode == Plan::MODE_MANDATE) { $data = $response->getData(true); $data['checkout'] = $this->mandateForPlan($request->plan, $request->discount, $user); $response->setData($data); } return $response; } /** * Collects some content to display to the user before redirect to a checkout page. * Optionally creates a recurrent payment mandate for specified user/plan. */ protected function mandateForPlan(Plan $plan, Discount $discount = null, User $user = null): array { $result = []; $min = \App\Payment::MIN_AMOUNT; $planCost = $cost = $plan->cost(); $disc = 0; if ($discount) { // Free accounts don't need the auto-payment mandate // Note: This means the voucher code is the only point of user verification if ($discount->discount == 100) { return [ 'content' => self::trans('app.signup-account-free'), 'cost' => 0, ]; } $planCost = (int) ($planCost * (100 - $discount->discount) / 100); $disc = $cost - $planCost; } if ($planCost > $min) { $min = $planCost; } if ($user) { $wallet = $user->wallets()->first(); $wallet->setSettings([ 'mandate_amount' => sprintf('%.2f', round($min / 100, 2)), 'mandate_balance' => 0, ]); $mandate = [ 'currency' => $wallet->currency, 'description' => \App\Tenant::getConfig($user->tenant_id, 'app.name') . ' ' . self::trans('app.mandate-description-suffix'), 'methodId' => PaymentProvider::METHOD_CREDITCARD, 'redirectUrl' => Utils::serviceUrl('/payment/status', $user->tenant_id), ]; $provider = PaymentProvider::factory($wallet); $result = $provider->createMandate($wallet, $mandate); } $country = Utils::countryForRequest(); $period = $plan->months == 12 ? 'yearly' : 'monthly'; $currency = \config('app.currency'); $rate = VatRate::where('country', $country) ->where('start', '<=', now()->format('Y-m-d h:i:s')) ->orderByDesc('start') ->limit(1) ->first(); $summary = '' . '' . self::trans("app.signup-subscription-{$period}") . '' . '' . Utils::money($cost, $currency) . '' . ''; if ($discount) { $summary .= '' . '' . self::trans('app.discount-code', ['code' => $discount->code]) . '' . '' . Utils::money(-$disc, $currency) . '' . ''; } $summary .= '' . '' . '' . self::trans('app.total') . '' . '' . Utils::money($planCost, $currency) . '' . ''; if ($rate && $rate->rate > 0) { // TODO: app.vat.mode $vat = round($planCost * $rate->rate / 100); $content = self::trans('app.vat-incl', [ 'rate' => Utils::percent($rate->rate), 'cost' => Utils::money($planCost - $vat, $currency), 'vat' => Utils::money($vat, $currency), ]); $summary .= '*' . $content . ''; } $trialEnd = $plan->free_months ? now()->copy()->addMonthsWithoutOverflow($plan->free_months) : now(); $params = [ 'cost' => Utils::money($planCost, $currency), 'date' => $trialEnd->toDateString(), ]; $result['title'] = self::trans("app.signup-plan-{$period}"); $result['content'] = self::trans('app.signup-account-mandate', $params); $result['summary'] = '' . $summary . '
'; $result['cost'] = $planCost; return $result; } /** * Returns plan for the signup process * * @returns \App\Plan Plan object selected for current signup process */ protected function getPlan() { $request = request(); if (!$request->plan || !$request->plan instanceof Plan) { // Get the plan if specified and exists... if (($request->code instanceof SignupCode) && $request->code->plan) { $plan = Plan::withEnvTenantContext()->where('title', $request->code->plan)->first(); } elseif ($request->plan) { $plan = Plan::withEnvTenantContext()->where('title', $request->plan)->first(); } // ...otherwise use the default plan if (empty($plan)) { // TODO: Get default plan title from config $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); } $request->plan = $plan; } return $request->plan; } /** * Login (kolab identity) validation * * @param string $login Login (local part of an email address) * @param string $domain Domain name * @param bool $external Enables additional checks for domain part * * @return array Error messages on validation error */ protected static function validateLogin($login, $domain, $external = false): ?array { // Validate login part alone $v = Validator::make( ['login' => $login], ['login' => ['required', 'string', new UserEmailLocal($external)]] ); if ($v->fails()) { return ['login' => $v->errors()->toArray()['login'][0]]; } $domains = $external ? null : Domain::getPublicDomains(); // Validate the domain $v = Validator::make( ['domain' => $domain], ['domain' => ['required', 'string', new UserEmailDomain($domains)]] ); if ($v->fails()) { return ['domain' => $v->errors()->toArray()['domain'][0]]; } $domain = Str::lower($domain); // Check if domain is already registered with us if ($external) { if (Domain::withTrashed()->where('namespace', $domain)->exists()) { return ['domain' => self::trans('validation.domainexists')]; } } // Check if user with specified login already exists $email = $login . '@' . $domain; if (User::emailExists($email) || User::aliasExists($email) || \App\Group::emailExists($email)) { return ['login' => self::trans('validation.loginexists')]; } return null; } } diff --git a/src/app/Http/Controllers/API/V4/MeetController.php b/src/app/Http/Controllers/API/V4/MeetController.php index e4d03df8..bbb10fbf 100644 --- a/src/app/Http/Controllers/API/V4/MeetController.php +++ b/src/app/Http/Controllers/API/V4/MeetController.php @@ -1,190 +1,191 @@ first(); // Room does not exist, or the owner is deleted if (!$room || !($wallet = $room->wallet()) || !$wallet->owner || $wallet->owner->isDegraded(true)) { return $this->errorResponse(404, self::trans('meet.room-not-found')); } $user = Auth::guard()->user(); $isOwner = $user && ( $user->id == $wallet->owner->id || $room->permissions()->where('user', $user->email)->exists() ); $init = !empty(request()->input('init')); // There's no existing session if (!$room->hasSession()) { // Participants can't join the room until the session is created by the owner if (!$isOwner) { return $this->errorResponse(422, self::trans('meet.session-not-found'), ['code' => 323]); } // The room owner can create the session on request if (!$init) { return $this->errorResponse(422, self::trans('meet.session-not-found'), ['code' => 324]); } $session = $room->createSession(); if (empty($session)) { return $this->errorResponse(500, self::trans('meet.session-create-error')); } } $settings = $room->getSettings(['locked', 'nomedia', 'password']); $password = (string) $settings['password']; $config = [ 'locked' => $settings['locked'] === 'true', 'nomedia' => $settings['nomedia'] === 'true', 'password' => $isOwner ? $password : '', 'requires_password' => !$isOwner && strlen($password), ]; $response = ['config' => $config]; // Validate room password if (!$isOwner && strlen($password)) { $request_password = request()->input('password'); if ($request_password !== $password) { - return $this->errorResponse(422, self::trans('meet.session-password-error'), $response + ['code' => 325]); + $response['code'] = 325; + return $this->errorResponse(422, self::trans('meet.session-password-error'), $response); } } // Handle locked room if (!$isOwner && $config['locked']) { $nickname = request()->input('nickname'); $picture = request()->input('picture'); $requestId = request()->input('requestId'); $request = $requestId ? $room->requestGet($requestId) : null; $error = self::trans('meet.session-room-locked-error'); // Request already has been processed (not accepted yet, but it could be denied) if (empty($request['status']) || $request['status'] != Room::REQUEST_ACCEPTED) { if (!$request) { if (empty($nickname) || empty($requestId) || !preg_match('/^[a-z0-9]{8,32}$/i', $requestId)) { return $this->errorResponse(422, $error, $response + ['code' => 326]); } if (empty($picture)) { $svg = file_get_contents(resource_path('images/user.svg')); $picture = 'data:image/svg+xml;base64,' . base64_encode($svg); } elseif (!preg_match('|^data:image/png;base64,[a-zA-Z0-9=+/]+$|', $picture)) { return $this->errorResponse(422, $error, $response + ['code' => 326]); } // TODO: Resize when big/make safe the user picture? $request = ['nickname' => $nickname, 'requestId' => $requestId, 'picture' => $picture]; if (!$room->requestSave($requestId, $request)) { // FIXME: should we use error code 500? return $this->errorResponse(422, $error, $response + ['code' => 326]); } // Send the request (signal) to all moderators $result = $room->signal('joinRequest', $request, Room::ROLE_MODERATOR); } return $this->errorResponse(422, $error, $response + ['code' => 327]); } } // Initialize connection tokens if ($init) { // Choose the connection role $canPublish = !empty(request()->input('canPublish')) && (empty($config['nomedia']) || $isOwner); $role = $canPublish ? Room::ROLE_PUBLISHER : Room::ROLE_SUBSCRIBER; if ($isOwner) { $role |= Room::ROLE_MODERATOR; $role |= Room::ROLE_OWNER; } // Create session token for the current user/connection $response = $room->getSessionToken($role); if (empty($response)) { return $this->errorResponse(500, self::trans('meet.session-join-error')); } $response_code = 200; $response['role'] = $role; $response['config'] = $config; } else { $response_code = 422; $response['code'] = 322; } return response()->json($response, $response_code); } /** * Webhook as triggered from the Meet server * * @param \Illuminate\Http\Request $request The API request. * * @return \Illuminate\Http\Response The response */ public function webhook(Request $request) { \Log::debug($request->getContent()); // Authenticate the request if ($request->headers->get('X-Auth-Token') != \config('meet.webhook_token')) { return response('Unauthorized', 403); } $sessionId = (string) $request->input('roomId'); $event = (string) $request->input('event'); switch ($event) { case 'roomClosed': // When all participants left the room the server will dispatch roomClosed // event. We'll remove the session reference from the database. $room = Room::where('session_id', $sessionId)->first(); if ($room) { $room->session_id = null; $room->save(); } break; case 'joinRequestAccepted': case 'joinRequestDenied': $room = Room::where('session_id', $sessionId)->first(); if ($room) { $method = $event == 'joinRequestAccepted' ? 'requestAccept' : 'requestDeny'; $room->{$method}($request->input('requestId')); } break; } return response('Success', 200); } } diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php index 7724dec2..22bf16dc 100644 --- a/src/app/Jobs/User/CreateJob.php +++ b/src/app/Jobs/User/CreateJob.php @@ -1,115 +1,116 @@ isDeleted()`), or * * the user is actually deleted (`$user->deleted_at`), or * * the user is already marked as ready in LDAP (`$user->isLdapReady()`). * */ class CreateJob extends UserJob { /** * Execute the job. * * @return void * * @throws \Exception */ public function handle() { $user = $this->getUser(); if (!$user) { return; } if ($user->role) { // Admins/resellers don't reside in LDAP (for now) return; } if ($user->email == \config('imap.admin_login')) { // Ignore Cyrus admin account return; } // sanity checks if ($user->isDeleted()) { $this->fail(new \Exception("User {$this->userId} is marked as deleted.")); return; } if ($user->trashed()) { $this->fail(new \Exception("User {$this->userId} is actually deleted.")); return; } $withLdap = \config('app.with_ldap'); // see if the domain is ready $domain = $user->domain(); if (!$domain) { $this->fail(new \Exception("The domain for {$this->userId} does not exist.")); return; } if ($domain->isDeleted()) { $this->fail(new \Exception("The domain for {$this->userId} is marked as deleted.")); return; } if ($withLdap && !$domain->isLdapReady()) { $this->release(60); return; } if (\config('abuse.suspend_enabled') && !$user->isSuspended()) { $code = \Artisan::call("user:abuse-check {$this->userId}"); if ($code == 2) { \Log::info("Suspending user due to suspected abuse: {$this->userId} {$user->email}"); $user->status |= \App\User::STATUS_SUSPENDED; } } if ($withLdap && !$user->isLdapReady()) { \App\Backends\LDAP::createUser($user); $user->status |= \App\User::STATUS_LDAP_READY; $user->save(); } if (!$user->isImapReady()) { if (\config('app.with_imap')) { if (!\App\Backends\IMAP::createUser($user)) { throw new \Exception("Failed to create mailbox for user {$this->userId}."); } } else { if (!\App\Backends\IMAP::verifyAccount($user->email)) { $this->release(15); return; } } $user->status |= \App\User::STATUS_IMAP_READY; } // Make user active in non-mandate mode only - if (!($wallet = $user->wallet()) + if ( + !($wallet = $user->wallet()) || !($plan = $user->wallet()->plan()) || $plan->mode != \App\Plan::MODE_MANDATE ) { $user->status |= \App\User::STATUS_ACTIVE; } $user->save(); } }