belongsTo(Role::class, 'roles_id'); } public function request(): HasMany { return $this->hasMany(UserRequest::class, 'users_id'); } public function download(): HasMany { return $this->hasMany(UserDownload::class, 'users_id'); } public function release(): HasMany { return $this->hasMany(UsersRelease::class, 'users_id'); } public function series(): HasMany { return $this->hasMany(UserSerie::class, 'users_id'); } public function invitation(): HasMany { return $this->hasMany(Invitation::class, 'invited_by'); } public function failedRelease(): HasMany { return $this->hasMany(DnzbFailure::class, 'users_id'); } public function comment(): HasMany { return $this->hasMany(ReleaseComment::class, 'users_id'); } public function promotionStats(): HasMany { return $this->hasMany(RolePromotionStat::class); } public function roleHistory(): HasMany { return $this->hasMany(UserRoleHistory::class); } /** * Check if user has a pending stacked role */ public function hasPendingRole(): bool { return $this->pending_roles_id !== null && $this->pending_role_start_date !== null; } /** * Get the pending role details */ public function getPendingRole(): ?Role { if (!$this->hasPendingRole()) { return null; } return Role::find($this->pending_roles_id); } /** * Cancel a pending stacked role */ public function cancelPendingRole(): bool { if (!$this->hasPendingRole()) { return false; } return $this->update([ 'pending_roles_id' => null, 'pending_role_start_date' => null, ]); } /** * Get role expiry information including pending roles */ public function getRoleExpiryInfo(): array { $currentRole = $this->role; $currentExpiry = $this->rolechangedate ? Carbon::parse($this->rolechangedate) : null; $pendingRole = $this->getPendingRole(); $pendingStart = $this->pending_role_start_date ? Carbon::parse($this->pending_role_start_date) : null; return [ 'current_role' => $currentRole, 'current_expiry' => $currentExpiry, 'has_expiry' => $currentExpiry !== null, 'is_expired' => $currentExpiry ? $currentExpiry->isPast() : false, 'days_until_expiry' => $currentExpiry ? Carbon::now()->diffInDays($currentExpiry, false) : null, 'pending_role' => $pendingRole, 'pending_start' => $pendingStart, 'has_pending_role' => $this->hasPendingRole(), ]; } /** * Get the user's timezone or default to UTC */ public function getTimezone(): string { return $this->timezone ?? 'UTC'; } /** * @throws \Exception */ public static function deleteUser($id): void { self::find($id)->delete(); } public static function getCount(?string $role = null, ?string $username = '', ?string $host = '', ?string $email = '', ?string $createdFrom = '', ?string $createdTo = ''): int { $res = self::query()->withTrashed()->where('email', '<>', 'sharing@nZEDb.com'); if (! empty($role)) { $res->where('roles_id', $role); } if ($username !== '') { $res->where('username', 'like', '%'.$username.'%'); } if ($host !== '') { $res->where('host', 'like', '%'.$host.'%'); } if ($email !== '') { $res->where('email', 'like', '%'.$email.'%'); } if ($createdFrom !== '') { $res->where('created_at', '>=', $createdFrom.' 00:00:00'); } if ($createdTo !== '') { $res->where('created_at', '<=', $createdTo.' 23:59:59'); } return $res->count(['id']); } public static function updateUser(int $id, string $userName, ?string $email, int $grabs, int $role, ?string $notes, int $invites, int $movieview, int $musicview, int $gameview, int $xxxview, int $consoleview, int $bookview, string $style = 'None'): int { $userName = trim($userName); $rateLimit = Role::query()->where('id', $role)->first(); $sql = [ 'username' => $userName, 'grabs' => $grabs, 'roles_id' => $role, 'notes' => substr($notes, 0, 255), 'invites' => $invites, 'movieview' => $movieview, 'musicview' => $musicview, 'gameview' => $gameview, 'xxxview' => $xxxview, 'consoleview' => $consoleview, 'bookview' => $bookview, 'style' => $style, 'rate_limit' => $rateLimit ? $rateLimit['rate_limit'] : 60, ]; if (! empty($email)) { $email = trim($email); $sql += ['email' => $email]; } $user = self::find($id); $user->update($sql); $user->syncRoles([$rateLimit['name']]); return self::SUCCESS; } /** * @return User|Builder|Model|object|null */ public static function getByUsername(string $userName) { return self::whereUsername($userName)->first(); } /** * @param string $email * @return Model|static * */ public static function getByEmail(string $email): Model|static { return self::whereEmail($email)->first(); } public static function updateUserRole(int $uid, int|string $role, bool $applyPromotions = true, bool $stackRole = true, ?int $changedBy = null, ?string $originalExpiryBeforeEdits = null, bool $preserveCurrentExpiry = false, ?int $addYears = null): bool { // Handle role parameter - can be int, numeric string, or role name if (is_numeric($role)) { $roleQuery = Role::query()->where('id', (int) $role)->first(); } else { $roleQuery = Role::query()->where('name', $role)->first(); } if (!$roleQuery) { Log::error('Role not found', ['role' => $role, 'role_type' => gettype($role)]); return false; } $roleName = $roleQuery->name; $user = self::find($uid); if (!$user) { Log::error('User not found', ['uid' => $uid]); return false; } $currentRoleId = $user->roles_id; // Use the original expiry from before any edits if provided, otherwise use current $oldExpiryDate = $originalExpiryBeforeEdits ? Carbon::parse($originalExpiryBeforeEdits) : ($user->rolechangedate ? Carbon::parse($user->rolechangedate) : null); // The current expiry (after any updates in controller) is what we use for stacking logic $currentExpiryDate = $user->rolechangedate ? Carbon::parse($user->rolechangedate) : null; Log::info('User current state', [ 'currentRoleId' => $currentRoleId, 'oldExpiryDate' => $oldExpiryDate?->toDateTimeString(), 'currentExpiryDate' => $currentExpiryDate?->toDateTimeString(), 'oldExpiryIsFuture' => $oldExpiryDate?->isFuture(), 'currentExpiryIsFuture' => $currentExpiryDate?->isFuture() ]); // Check if role is actually changing if ($currentRoleId === $roleQuery->id) { Log::info('Role not changing, but checking for promotions', [ 'applyPromotions' => $applyPromotions, 'currentRoleId' => $currentRoleId ]); // Define roles that should not receive promotions $excludedRoles = ['User', 'Admin', 'Moderator', 'Disabled', 'Friend']; // Even if a role isn't changing, apply promotions if requested if ($applyPromotions && !in_array($roleName, $excludedRoles, true)) { $promotionDays = RolePromotion::calculateAdditionalDays($roleQuery->id); if ($promotionDays > 0) { Log::info('Applying promotion days to existing role', [ 'promotionDays' => $promotionDays, 'currentExpiryDate' => $currentExpiryDate?->toDateTimeString() ]); // Calculate a new expiry date by adding promotion days $newExpiryDate = null; if ($currentExpiryDate) { // Extend from the current expiry date $newExpiryDate = $currentExpiryDate->copy()->addDays($promotionDays); } else { // No expiry date, create one from now $newExpiryDate = Carbon::now()->addDays($promotionDays); } // Update the user's expiry date $updated = $user->update([ 'rolechangedate' => $newExpiryDate, ]); Log::info('Updated expiry date with promotions', [ 'updated' => $updated, 'new_expiry_date' => $newExpiryDate?->toDateTimeString() ]); // Track promotion statistics if ($updated) { $promotions = RolePromotion::getActivePromotions($roleQuery->id); foreach ($promotions as $promotion) { $promotion->trackApplication( $user->id, $roleQuery->id, $currentExpiryDate, $newExpiryDate ); } Log::info('Tracked promotion application for existing role', [ 'promotions_count' => $promotions->count() ]); } return $updated; } } Log::info('No promotions to apply, returning'); return true; // No change needed } // Determine if we should stack this role change (based on CURRENT expiry, not old) $shouldStack = $stackRole && $currentExpiryDate && $currentExpiryDate->isFuture(); Log::info('Stack decision', [ 'shouldStack' => $shouldStack, 'stackRole' => $stackRole, 'hasCurrentExpiry' => $currentExpiryDate !== null, 'isFuture' => $currentExpiryDate?->isFuture() ]); if ($shouldStack) { Log::info('Stacking role change', [ 'oldExpiryDate' => $oldExpiryDate->toDateTimeString(), 'currentExpiryDate' => $currentExpiryDate->toDateTimeString(), 'pendingRoleStartDate' => $oldExpiryDate->toDateTimeString() ]); // Calculate a new expiry date for the pending role // Start with the role's base duration (addyears field converted to days) // Use the provided addYears parameter if available, otherwise use role's default $baseDays = ($addYears !== null ? $addYears : $roleQuery->addyears) * 365; $promotionDays = 0; // Define roles that should not receive promotions $excludedRoles = ['User', 'Admin', 'Moderator', 'Disabled', 'Friend']; if ($applyPromotions && !in_array($roleName, $excludedRoles, true)) { $promotionDays = RolePromotion::calculateAdditionalDays($roleQuery->id); } $totalDays = $baseDays + $promotionDays; // New role will start at oldExpiryDate (original expiry date before any admin edits) // Then add the total days (base + promotion) to calculate when it will expire $newExpiryDate = $oldExpiryDate->copy()->addDays($totalDays); Log::info('Calculated new expiry for pending role', [ 'pendingRoleStartDate' => $oldExpiryDate->toDateTimeString(), 'baseDays' => $baseDays, 'promotionDays' => $promotionDays, 'totalDays' => $totalDays, 'newExpiryDate' => $newExpiryDate->toDateTimeString() ]); // Stack the role change - set it as pending // Use oldExpiryDate (original expiry) for when the role will start $user->update([ 'pending_roles_id' => $roleQuery->id, 'pending_role_start_date' => $oldExpiryDate, ]); Log::info('Pending role updated', [ 'pending_roles_id' => $roleQuery->id, 'pending_role_start_date' => $oldExpiryDate->toDateTimeString(), 'pending_role_start_date_formatted' => $oldExpiryDate->format('Y-m-d H:i:s') ]); // Record in history as a stacked change // effective_date should match old_expiry_date (when the stacked role will start) try { $history = UserRoleHistory::recordRoleChange( userId: $user->id, oldRoleId: $currentRoleId, newRoleId: $roleQuery->id, oldExpiryDate: $oldExpiryDate, // When current role expires newExpiryDate: $newExpiryDate, // When the NEW role will expire (calculated from oldExpiryDate) effectiveDate: $oldExpiryDate, // Same as old_expiry_date - when the new role starts isStacked: true, changeReason: 'stacked_role_change', changedBy: $changedBy ); Log::info('Role history recorded for stacked change', [ 'history_id' => $history->id, 'effective_date' => $history->effective_date->toDateTimeString(), 'old_expiry_date' => $history->old_expiry_date?->toDateTimeString(), 'new_expiry_date' => $history->new_expiry_date?->toDateTimeString(), 'note' => 'effective_date equals old_expiry_date (when stacked role starts)' ]); } catch (\Exception $e) { Log::error('Failed to record role history', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); } return true; } // Apply the role change immediately // Calculate base days from role's addyears field // Use the provided addYears parameter if available, otherwise use role's default $baseDays = ($addYears !== null ? $addYears : $roleQuery->addyears) * 365; $promotionDays = 0; // Define roles that should not receive promotions $excludedRoles = ['User', 'Admin', 'Moderator', 'Disabled', 'Friend']; if ($applyPromotions && !in_array($roleName, $excludedRoles, true)) { $promotionDays = RolePromotion::calculateAdditionalDays($roleQuery->id); } $totalDays = $baseDays + $promotionDays; Log::info('Applying immediate role change', [ 'baseDays' => $baseDays, 'promotionDays' => $promotionDays, 'totalDays' => $totalDays, 'applyPromotions' => $applyPromotions ]); // Calculate new expiry date $newExpiryDate = null; if ($preserveCurrentExpiry && $currentExpiryDate) { // Admin manually set an expiry date - preserve it as-is $newExpiryDate = $currentExpiryDate; Log::info('Preserving admin-set expiry date', [ 'preservedDate' => $newExpiryDate->toDateTimeString() ]); } elseif ($totalDays > 0) { // For immediate changes, start from now (not from old expiry) $baseDate = Carbon::now(); $newExpiryDate = $baseDate->copy()->addDays($totalDays); Log::info('Calculated new expiry date', [ 'baseDate' => $baseDate->toDateTimeString(), 'totalDays' => $totalDays, 'newExpiryDate' => $newExpiryDate->toDateTimeString() ]); } // Update the user's role $updated = $user->update([ 'roles_id' => $roleQuery->id, 'rolechangedate' => $newExpiryDate, ]); Log::info('User role updated', [ 'updated' => $updated, 'new_roles_id' => $roleQuery->id, 'new_rolechangedate' => $newExpiryDate?->toDateTimeString() ]); // Sync Spatie roles $user->syncRoles([$roleName]); // Record in history if ($updated) { try { $history = UserRoleHistory::recordRoleChange( userId: $user->id, oldRoleId: $currentRoleId, newRoleId: $roleQuery->id, oldExpiryDate: $oldExpiryDate, newExpiryDate: $newExpiryDate, effectiveDate: Carbon::now(), isStacked: false, changeReason: 'immediate_role_change', changedBy: $changedBy ); \Log::info('Role history recorded for immediate change', [ 'history_id' => $history->id, 'effective_date' => $history->effective_date->toDateTimeString(), 'old_expiry_date' => $history->old_expiry_date?->toDateTimeString(), 'new_expiry_date' => $history->new_expiry_date?->toDateTimeString() ]); } catch (\Exception $e) { Log::error('Failed to record role history for immediate change', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString() ]); } // Track promotion statistics if applicable if ($applyPromotions && $promotionDays > 0) { $promotions = RolePromotion::getActivePromotions($roleQuery->id); foreach ($promotions as $promotion) { $promotion->trackApplication( $user->id, $roleQuery->id, $oldExpiryDate, $newExpiryDate ); } } } return $updated; } public static function updateExpiredRoles(): void { $now = CarbonImmutable::now(); $period = [ 'day' => $now->addDay(), 'week' => $now->addWeek(), 'month' => $now->addMonth(), ]; foreach ($period as $value) { $users = self::query()->whereDate('rolechangedate', '=', $value)->get(); $days = $now->diffInDays($value, true); foreach ($users as $user) { SendAccountWillExpireEmail::dispatch($user, $days)->onQueue('emails'); } } // Process expired roles foreach (self::query()->whereDate('rolechangedate', '<', $now)->get() as $expired) { $oldRoleId = $expired->roles_id; $oldExpiryDate = $expired->rolechangedate ? Carbon::parse($expired->rolechangedate) : null; // Check if there's a pending stacked role if ($expired->pending_roles_id && $expired->pending_role_start_date) { $pendingStartDate = Carbon::parse($expired->pending_role_start_date); // If the pending role should start now or earlier if ($pendingStartDate->lte($now)) { // Apply the pending role $newRoleId = $expired->pending_roles_id; $roleQuery = Role::query()->where('id', $newRoleId)->first(); if ($roleQuery) { // Calculate base days from role's addyears field $baseDays = $roleQuery->addyears * 365; $promotionDays = RolePromotion::calculateAdditionalDays($newRoleId); $totalDays = $baseDays + $promotionDays; // Calculate new expiry date from now (when the role is being activated) $newExpiryDate = $totalDays > 0 ? $now->addDays($totalDays) : null; // Update user with pending role $expired->update([ 'roles_id' => $newRoleId, 'rolechangedate' => $newExpiryDate, 'pending_roles_id' => null, 'pending_role_start_date' => null, ]); $expired->syncRoles([$roleQuery->name]); // Record in history UserRoleHistory::recordRoleChange( userId: $expired->id, oldRoleId: $oldRoleId, newRoleId: $newRoleId, oldExpiryDate: $oldExpiryDate, newExpiryDate: $newExpiryDate, effectiveDate: Carbon::instance($now), isStacked: true, changeReason: 'stacked_role_activated', changedBy: null ); continue; // Skip the default expiry handling } } } // Default behavior: downgrade to User role $expired->update([ 'roles_id' => self::ROLE_USER, 'rolechangedate' => null, 'pending_roles_id' => null, 'pending_role_start_date' => null, ]); $expired->syncRoles(['User']); // Record in history UserRoleHistory::recordRoleChange( userId: $expired->id, oldRoleId: $oldRoleId, newRoleId: self::ROLE_USER, oldExpiryDate: $oldExpiryDate, newExpiryDate: null, effectiveDate: Carbon::instance($now), isStacked: false, changeReason: 'role_expired', changedBy: null ); SendAccountExpiredEmail::dispatch($expired)->onQueue('emails'); } } /** * @throws \Throwable */ public static function getRange($start, $offset, $orderBy, ?string $userName = '', ?string $email = '', ?string $host = '', ?string $role = '', bool $apiRequests = false, ?string $createdFrom = '', ?string $createdTo = ''): Collection { if ($apiRequests) { UserRequest::clearApiRequests(false); $query = " SELECT users.*, roles.name AS rolename, COUNT(user_requests.id) AS apirequests FROM users INNER JOIN roles ON roles.id = users.roles_id LEFT JOIN user_requests ON user_requests.users_id = users.id WHERE users.id != 0 %s %s %s %s %s %s AND email != 'sharing@nZEDb.com' GROUP BY users.id ORDER BY %s %s %s "; } else { $query = ' SELECT users.*, roles.name AS rolename FROM users INNER JOIN roles ON roles.id = users.roles_id WHERE 1=1 %s %s %s %s %s %s ORDER BY %s %s %s'; } $order = self::getBrowseOrder($orderBy); return self::fromQuery( sprintf( $query, ! empty($userName) ? 'AND users.username '.'LIKE '.escapeString('%'.$userName.'%') : '', ! empty($email) ? 'AND users.email '.'LIKE '.escapeString('%'.$email.'%') : '', ! empty($host) ? 'AND users.host '.'LIKE '.escapeString('%'.$host.'%') : '', (! empty($role) ? ('AND users.roles_id = '.$role) : ''), ! empty($createdFrom) ? 'AND users.created_at >= '.escapeString($createdFrom.' 00:00:00') : '', ! empty($createdTo) ? 'AND users.created_at <= '.escapeString($createdTo.' 23:59:59') : '', $order[0], $order[1], ($start === false ? '' : ('LIMIT '.$offset.' OFFSET '.$start)) ) ); } /** * Get sort types for sorting users on the web page user list. * * @return string[] */ public static function getBrowseOrder($orderBy): array { $order = (empty($orderBy) ? 'username_desc' : $orderBy); $orderArr = explode('_', $order); $orderField = match ($orderArr[0]) { 'email' => 'email', 'host' => 'host', 'createdat' => 'created_at', 'lastlogin' => 'lastlogin', 'apiaccess' => 'apiaccess', 'grabs' => 'grabs', 'role' => 'rolename', 'rolechangedate' => 'rolechangedate', 'verification' => 'verified', default => 'username', }; $orderSort = (isset($orderArr[1]) && preg_match('/^asc|desc$/i', $orderArr[1])) ? $orderArr[1] : 'desc'; return [$orderField, $orderSort]; } /** * Verify a password against a hash. * * Automatically update the hash if it needs to be. * * @param string $password Password to check against hash. * @param bool|string $hash Hash to check against password. * @param int $userID ID of the user. */ public static function checkPassword(string $password, bool|string $hash, int $userID = -1): bool { if (Hash::check($password, $hash) === false) { return false; } // Update the hash if it needs to be. if (is_numeric($userID) && $userID > 0 && Hash::needsRehash($hash)) { $hash = self::hashPassword($password); if ($hash !== false) { self::find($userID)->update(['password' => $hash]); } } return true; } public static function updateRssKey($uid): int { self::find($uid)->update(['api_token' => md5(Password::getRepository()->createNewToken())]); return self::SUCCESS; } public static function updatePassResetGuid($id, $guid): int { self::find($id)->update(['resetguid' => $guid]); return self::SUCCESS; } public static function updatePassword(int $id, string $password): int { self::find($id)->update(['password' => self::hashPassword($password)]); return self::SUCCESS; } public static function updateUserRoleChangeDate(int $id, string $roleChangeDate): int { self::find($id)->update(['rolechangedate' => $roleChangeDate]); return self::SUCCESS; } public static function hashPassword($password): string { return Hash::make($password); } /** * @return Model|static * * @throws ModelNotFoundException */ public static function getByPassResetGuid(string $guid) { return self::whereResetguid($guid)->first(); } public static function incrementGrabs(int $id, int $num = 1): void { self::find($id)->increment('grabs', $num); } /** * @return Model|null|static */ public static function getByRssToken(string $rssToken) { return self::whereApiToken($rssToken)->first(); } public static function isValidUrl($url): bool { return (! preg_match('/^(http|https|ftp):\/\/([A-Z0-9][A-Z0-9_-]*(?:\.[A-Z0-9][A-Z0-9_-]*)+):?(\d+)?\/?/i', $url)) ? false : true; } /** * @throws \Exception */ public static function generatePassword(int $length = 15): string { return Str::password($length); } /** * @throws \Exception */ public static function signUp($userName, $password, $email, $host, $notes, int $invites = Invitation::DEFAULT_INVITES, string $inviteCode = '', bool $forceInviteMode = false, int $role = self::ROLE_USER, bool $validate = true): bool|int|string { $user = [ 'username' => trim($userName), 'password' => trim($password), 'email' => trim($email), ]; if ($validate) { $validator = Validator::make($user, [ 'username' => ['required', 'string', 'min:5', 'max:255', 'unique:users'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users', new ValidEmailDomain()], 'password' => ['required', 'string', 'min:8', 'confirmed', 'regex:/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$/'], ]); if ($validator->fails()) { return implode('', Arr::collapse($validator->errors()->toArray())); } } // Make sure this is the last check, as if a further validation check failed, the invite would still have been used up. $invitedBy = 0; if (! $forceInviteMode && (int) Settings::settingValue('registerstatus') === Settings::REGISTER_STATUS_INVITE) { if ($inviteCode === '') { return self::ERR_SIGNUP_BADINVITECODE; } $invitedBy = self::checkAndUseInvite($inviteCode); if ($invitedBy < 0) { return self::ERR_SIGNUP_BADINVITECODE; } } return self::add($user['username'], $user['password'], $user['email'], $role, $notes, $host, $invites, $invitedBy); } /** * If a invite is used, decrement the person who invited's invite count. */ public static function checkAndUseInvite(string $inviteCode): int { $invite = Invitation::findValidByToken($inviteCode); if (! $invite) { return -1; } self::query()->where('id', $invite->invited_by)->decrement('invites'); $invite->markAsUsed(0); // Will be updated with actual user ID later return $invite->invited_by; } /** * @return false|int|mixed */ public static function add(string $userName, string $password, string $email, int $role, ?string $notes = '', string $host = '', int $invites = Invitation::DEFAULT_INVITES, int $invitedBy = 0) { $password = self::hashPassword($password); if (! $password) { return false; } $storeips = config('nntmux:settings.store_user_ip') === true ? $host : ''; $user = self::create( [ 'username' => $userName, 'password' => $password, 'email' => $email, 'host' => $storeips, 'roles_id' => $role, 'invites' => $invites, 'invitedby' => (int) $invitedBy === 0 ? null : $invitedBy, 'notes' => $notes, ] ); return $user->id; } /** * Get the list of categories the user has excluded. * * @param int $userID ID of the user. * * @throws \Exception */ public static function getCategoryExclusionById(int $userID): array { $ret = []; $user = self::find($userID); $userAllowed = $user->getDirectPermissions()->pluck('name')->toArray(); $roleAllowed = $user->getAllPermissions()->pluck('name')->toArray(); $allowed = array_intersect($roleAllowed, $userAllowed); $cats = ['view console', 'view movies', 'view audio', 'view tv', 'view pc', 'view adult', 'view books', 'view other']; if (! empty($allowed)) { foreach ($cats as $cat) { if (! \in_array($cat, $allowed, false)) { $ret[] = match ($cat) { 'view console' => 1000, 'view movies' => 2000, 'view audio' => 3000, 'view pc' => 4000, 'view tv' => 5000, 'view adult' => 6000, 'view books' => 7000, 'view other' => 1, }; } } } return Category::query()->whereIn('root_categories_id', $ret)->pluck('id')->toArray(); } /** * @throws \Exception */ public static function getCategoryExclusionForApi(Request $request): array { $apiToken = $request->has('api_token') ? $request->input('api_token') : $request->input('apikey'); $user = self::getByRssToken($apiToken); return self::getCategoryExclusionById($user->id); } /** * @throws \Exception */ public static function sendInvite($serverUrl, $uid, $emailTo): string { $user = self::find($uid); // Create invitation using our custom system $invitation = Invitation::createInvitation($emailTo, $user->id); $url = $serverUrl.'/register?token='.$invitation->token; // Send invitation email $invitationService = app(InvitationService::class); $invitationService->sendInvitationEmail($invitation); return $url; } /** * Deletes users that have not verified their accounts for 3 or more days. */ public static function deleteUnVerified(): void { static::whereVerified(0)->where('created_at', '<', now()->subDays(3))->delete(); } public function passwordSecurity(): HasOne { return $this->hasOne(PasswordSecurity::class); } public static function canPost($user_id): bool { // return true if can_post column is true and false if can_post column is false return self::where('id', $user_id)->value('can_post'); } }