diff --git a/.env.example b/.env.example index 1836201..8d8b34d 100755 --- a/.env.example +++ b/.env.example @@ -8,6 +8,14 @@ APP_AUTH_BANNER="" APP_SITEHOMEPAGE="" API_PREFIX="api" +# Add legal document links here. You may also leave them empty. +GUIDELINES_URL="#" +PRIVACY_URL="#" +TERMS_URL="#" +SOURCE_REPO="#" +SUPPORT_EMAIL= +SUPPORT_URL= + # The auth banner is a relative path # Hides IP addresses @@ -30,10 +38,6 @@ DB_DATABASE=laravel DB_USERNAME=root DB_PASSWORD= -# Bot token for the Discord notification integration. -# Create an application & bot here: https://discord.com/developers/applications -DISCORD_BOT_TOKEN= - RECAPTCHA_SITE_KEY= RECAPTCHA_PRIVATE_KEY= RECAPTCHA_VERIFY_URL="https://www.google.com/recaptcha/api/siteverify" @@ -45,11 +49,28 @@ MOJANG_API_URL="https://api.mojang.com" IPGEO_API_KEY="" IPGEO_API_URL="https://api.ipgeolocation.io/ipgeo" +DISCORD_CLIENT_ID= +DISCORD_CLIENT_SECRET= +DISCORD_BOT_TOKEN= + +# Don't touch the base url +DISCORD_HOME_GUILD= +DISCORD_STAFF_GUILD= +DISCORD_BASE_URL="https://discord.com/api/v9" + +# Defines the basic set of server staff permissions that members should get in each server +# Add your staff role IDs here in a comma-delimited list, like so: "ROLEID1, ROLEID2, ROLEID3" and so on +# Specific permissions can be optionally configured for each vacancy +# For instance, here you may configure role IDs for example role "Server staff", while in a specific vacancy, you may configure example role "Mod" +DISCORD_STAFF_GUILD_ROLES= +DISCORD_HOME_GUILD_ROLES= + ARCANEDEV_LOGVIEWER_MIDDLEWARE=web,auth,can:admin.maintenance.logs.view RELEASE=0.8.0 SLACK_INTEGRATION_WEBHOOK= +CPANEL_API_TOKEN= BROADCAST_DRIVER=log CACHE_DRIVER=file diff --git a/.gitignore b/.gitignore index a97e37d..ecc828b 100755 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,6 @@ Homestead.yaml npm-debug.log yarn-error.log /.idea/ -/.idea/ +_ide_helper.php +_ide_helper_models.php +.phpstorm.meta.php diff --git a/Procfile b/Procfile deleted file mode 100755 index 48aab52..0000000 --- a/Procfile +++ /dev/null @@ -1 +0,0 @@ -web: vendor/bin/heroku-php-apache2 public/ diff --git a/README.md b/README.md index 8da10c7..f26fb73 100755 --- a/README.md +++ b/README.md @@ -1,16 +1,12 @@ -# RB Recruiter v 0.8.0 [![Crowdin](https://badges.crowdin.net/raspberry-staff-manager/localized.svg)](https://crowdin.com/project/raspberry-staff-manager) [![Better Uptime Badge](https://betteruptime.com/status-badges/v1/monitor/9n53.svg)](https://betteruptime.com/?utm_source=status_badge) -## The quick and pain-free form management solution for communities +# RBRecruiter v1.0.0-stable - Integrated Human Resources Management System -Have you ever gotten tired of managing your Minecraft server/network's applications through Discord (or anything else) and having to scroll through hundreds of new messages just to find that one applicant's username? - - -Wish you had a better application managemet strategy? Well, then RBRecruiter is for you! It was originally designed and developed for internal use for a gameserver network, but sharing is caring! +**Are the big name HR apps too bloated and overkill for your online community? Check RBRecruiter out to see if its right for you!** +Have you ever gotten tired of managing your community's staff member applications through Discord (or anything else) and having to scroll through hundreds of new messages just to find that one applicant's username? Struggling with missing/hard-to-find information? Then RBRecruiter is for you. Streamline your team member vetting/addition process by integrating RBRecruiter with your community, now with custom-made integrations to connect with several social platforms. # Features (not exhaustive) - - Beautiful (customizable in future releases) landing page for your application management center; It displays all available staff ranks - - Contact form on landing page for those un-registerd users + - Beautiful (customizable in future releases) landing page for your application management center; It displays all available positions/jobs - User registration/authentication system; Users will be sent to the authentication flow to complete their application, if not logged in - Candidate tracking system - Applicants will be tracked from start to finish. - Peer approval system - Have all your staff members vote on applications and decide whether they should be accepted (this is overridable) @@ -18,12 +14,11 @@ Wish you had a better application managemet strategy? Well, then RBRecruiter is - Interview notes: Every staff member is able to add and edit interview notes (how the interview went, etc) - Application comments: Finally no more having to go to a private Discord channel just to comment on a single application. Comments are organised neatly for every application! This should help in the decision process of voting for an application. - User profiles - Fill out your profile for others to better find you - - User directory - Public profile directory for everyone - - Staff rank management - Add/remove ranks on demand, that users will be able to apply to + - Vacancy management - Add/remove vacancies on demand, that users will be able to apply to - Simple form builder - Create your application forms easily! - - Termination - Has a staff member met their untimely demise? Terminate them. This will strip their permissions and roles. + - Termination - Has a staff member met their untimely demise? Terminate them. This will strip their permissions and roles, and run any other actions for your configured integration - Controllable permissions - Every user has permissions! Control who has access to what (You can skip the application process and add staff members directly here). - - Ban system - Having trouble with pesky spammers? Ban them! This will publicly shame their profile and keep them from signing up or logging in. + - Account suspension system - Having trouble with pesky spammers? Suspend them! This will keep them from signing up or signing in until you lift their suspension. - Notifications: Notifies slack and email primarily (Slack notifications currently broken) And many more features! @@ -31,11 +26,10 @@ Wish you had a better application managemet strategy? Well, then RBRecruiter is # Roadmap Many other features are currently planned for this app, such as: - - Discord role management (approved applicants) + - Discord role management (approved applicants) - Underway - Flexibility - This app is built on a flexible concept. - Customisable front page - Auto provisioning - Sign up on a website and get your instance of RBRecruiter up and running in no time (SaaS) - - - Suggestions accepted! @@ -63,8 +57,8 @@ Tech stack: # Software Requirements - ``composer`` (tested w/ v2.1.14) - - ``npm`` (tested w/ v 8.1.2) - - ``node`` (min version v16.13.2) + - ``npm`` (tested w/ v 8.19.2) + - ``node`` (min version v18.9.0) - ``php`` (required PHP 8.1 or newer - lower versions unsupported!) # PHP Extension Requirements @@ -87,4 +81,4 @@ This process will be moved to the browser later. # Bug reports -Please report any bugs you find to the issues section here! It'd be immensely helpful. PRs are also accepted. +Please report any bugs you find to the issues section. diff --git a/SECURITY.md b/SECURITY.md index 17c6d58..d7210c2 100755 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,25 +5,28 @@ The following versions are currently supported: | Version | Supported | -| ------- | ------------------ | +|---------|--------------------| | 0.1.x | :x: | | 0.5.x | :x: | | 0.6.x | :x: | | 0.7.0 | :x: | -| 0.7.1 | :white_check_mark: | +| 0.7.1 | :x: | +| 0.8.0 | :x: | +| 0.8.1 | :x: | +| 0.8.2 | :white_check_mark: | +| 0.9.0 | :white_check_mark: | + +Please note that all current versions are pre-release versions. This means that, until version `1.0.0` is reached, anything may change or break at any time, and releases are made often. +However, all commits releases are thoroughly tested before they are pushed out, ensuring that the `main` branch is never broken. ## Supported PHP Versions | Version | Supported | -| ------- | ------------------ | +|---------|--------------------| | 5.x | :x: | -| 7.0 | :x: | -| 7.1 | :x: | -| 7.2 | :x: | -| 7.3 | :x: | -| 7.4 | :white_check_mark: | +| 7.x | :x: | | 8.0 | :white_check_mark: | - +| 8.1 | :white_check_mark: | ## Supported Operating Systems @@ -40,75 +43,22 @@ The following versions are currently supported: If you found a critical vunlerability, please do not use the Issues tab to report it. Instead, please forward an encrypted vulnerability report to ``security@webvokestudio.pt``, so that we have time to open a security advisory and work on a fix. -Unencrypted reports will be ignored. Use this public PGP key to encrypt your message: -----BEGIN PGP PUBLIC KEY BLOCK----- - mQINBF/NUL8BEAC7njBq5IbB80qS8rGaAw+DM9wCTiZBiZu0oLXh6oW/E6NOKX5D - Gd8tcW2auqcO1syBUnla4E8t+3fuJQtee3lgszLKRKACYOqZPIYNBUHbw3VBNhNZ - WESbKh9JxlLJql6fHMfpr3wcTCxLwFGcRln1EPKAMBsxB0YJDeorXuoCTY4+NhGM - 8O2aS8Tbq+HxSkhz1p0DdOuaM9C/kQJkOth6sdsBiBE/OsyzPK/yti91WM9fB/Fg - QuYyzFD5OL6H+PKPas/4ndIfXSnSCeqm4gScUBeLReeqQd0o96ALSnOxMz9Qit4j - BYhKxfZnAFiKWeBPZEJY12cu2gv3plkdE89mqJveRUCQ+dfVX471iCW+/YSRGU83 - 9PG2a1Dalsx46lScHm2RlrXFhUhpVND0y7VBHpKiUJCoykhGbIAr429dITBw7jGa - FAKpJgm2wWRAHeokrQ9l+NTUosTwRoYeWF1tKElKT1sjF5zuWFxOekzGXBtzcSUH - HQrUfzOQTMUciSZ4CjZjDNL1yYEwUOTr4IzNOj/jmKCo/Dbsix9V1LbTPjK8vP4l - jwLOL2dG0jpQRd5FRBWPukpF6LS25vQv0bMRZdsCfh/eOlMldfdiGpx8fN6dkxvm - H0TBdZklp3U91u+ooVIeF3Q1NWmB/tvNJOfFnerHOpDBTy/q5fpT6QaMkwARAQAB - tH5NaWd1ZWwgTm9ndWVpcmEgKElnbm9yZSBhbGwgbXkgb3RoZXIga2V5cyB1bmRl - ciBteSBuYW1lIGFuZCBlbWFpbCAobG9zdCB0aGUgcHJpdmF0ZSBrZXlzKS4pIDxt - aWd1ZWw0NTZAc3BhY2VqZXdlbC1ob3N0aW5nLmNvbT6JAlQEEwEKAD4WIQStTNAz - Se1uoPMcdjTkUwCM6Yt4igUCX81QvwIbLwUJC0c1AAULCQgHAgYVCgkICwIEFgID - AQIeAQIXgAAKCRDkUwCM6Yt4ikBCD/9sPlUItGT1tlFgdSdbLdPsdcb/lpK269zH - 0OoMSSKGKNL6RAY2R6++qdBRCC9jdzQFtJO7epYB/mWXnZqYcWemTwaKEW5eHr5E - BFQuSiRREbNLkzCAWWqqaQnCuve4q9DXsWd5NiHcrmNgnSY6Zk6y+GdpQNyzEshf - aCcnkaySIFgV33DDsa6s4zUDZulQbmeg1mfwyP0tv+CZ6Bbk72DxX/A10x00ddv1 - arGuLfctIydEcG7mAeAZBeCJEucpFuQbaWybZS2RX3/e6LCOWnt/ZHjqcFln36yq - 9GIx28BnElkpxwDV66R0EUpm3cxZ3Kv7bDqm5E2ZzCybOF+a8y3d15qIyrSHvGdr - skVBmRi2zLxCYBEcTocHH0rG6UNTk55wvqMepAnnp12XMQoqCrmw9zUv92qAv+01 - lMv9H1AM8jUdkk5H+SgZYFcPL5/4cBEc/ylRKIuiNcKZAfppjdVnslXsgahmQSep - PgcWPwwOujqYLfn/FPKRQuH1GUuNv0UzO6KUQy0gWkDYKrnks4n6lNWXiZY/HU+W - KCgSWRCICHW8JJ8dg97vk6IurtqpG/LUMb/E3mAzPNIyuhK2r1wGOsb83iVIvF5M - 3KyAFhaqiwnudt7UXYN3A1we+rm/y/nBNx0GUuC8VJaLz1p9zqEiT1an0vivC92x - m/jSicdIprkCDQRfzVC/ARAAq/tqXnL7JSt3ZaePLP86hE0gOK8mY1F5X9sRQCDT - adqyfnwNIzXGhUFfSdIO2LKnrdZUFXZG4YeK6JG0fu8KdELs+2UlIx0KAokrwfK3 - zWHITN6+lvSz3ubVQMYCEgUK9eEPwa/XnhL4vEd61kMdM5OxYsBXQYlraWLJ0rOf - ANQEXb0vm0c4RsO8IubOJT7jxPkgfLesIB7i3O3/Hptc54WHs97p22/Y6oNk42UV - cJB/k3kqTCzfBzDcpmF9RyxpfEDUJAn9QWcxH2UC3kHpr9N5Hfv3Kh3bjaGukzn4 - sO+fmukqBTsW7cIMO7slbcsCVJN7QC2GcocgjGHXWVF1Gg234exTX7aDBGNq7TeY - h1N/3lVeZom6ANtA3aOO55NuaXI9IGiW7lNnYcTJkTu3eUnfTpg/FkqdSzOpBOFZ - Gyltjf8pMK0vQXbajov5j/wE/bz2eOPKwyJc0u3B/JqfE/EbPnx5dng7/mgv+x7u - GSMOvovc4SrSZoDp5IWDk1L5s2CHNB88rziPFLB0Tkr6N9MnEWhv4wL7wfNdQ00s - xJpGlrc0nSdGUmHFCExRUb7VIJ0Yfjsj3Xy4PCBXsDTYWusAbp+rbFycRMlGiANj - cQYGbEYMyUqL2upg7kKzrFW45TyGpkE1QHkm0Od7JxGqgfiie+tcG+EUQKBMJsIN - tLMAEQEAAYkEcgQYAQoAJhYhBK1M0DNJ7W6g8xx2NORTAIzpi3iKBQJfzVC/Ahsu - BQkLRzUAAkAJEORTAIzpi3iKwXQgBBkBCgAdFiEEqncyNKRovrMCcN8NLPYbglMW - xqAFAl/NUL8ACgkQLPYbglMWxqBmHhAApxjJmpdifzI23AdRAcO4oChE6ho0a85+ - nGXr0qPMySb6/1GiYML3WKNzK02wS7V8A1VlnYvdd3SdmJh9qhH4NAgs4SXxpenp - hEw48M+h7LHJlqkrftHC540eyNlyVyN31kNhibhFrF5eZ9BcIwERrAR4MClG1gwI - bJqiAk5wlYP6YmJHVI+4DKjN+dMCn301hXUCTPoAdHfKTztMe9qhmbgZcyeOkZOr - Ya/7D1AqEaQhfcfOL4mBUdH86GVw9o58Ock7baQHoYlIrwgvW4qRvBjT+mQueHld - J0v/GQM6widHe3vt3HL4vI4vTzoscl8MY4u8YCmvkWGh343G6Ei7m+cQ19GrHSOR - 8rMZjHbYaNHstscBJDVx19EZaGc/AV6w38MdkiXbKyyKkZuvAqWkgs+NFRE7lBtI - mNp40aakYNwnn0RP6Wz2LYRWcP5Zm79cUb8++CYK8h5Y5j53tlM8ihF58UhKL6rg - zEk5vYoJzmswO9nA9nNnPscZhZUwfE1x8SVtOOhr8IFlJ3Kh2BpTXJbmqwj71Y6x - pfP6GG4UmPzfLi64ZkvYU1l7nyxlbvJXd8LzBs5oofROiIYKQkrSMEZvPb8XUVNs - cMz434hQFPtwkygjtpA1Io+bx1BV4wtnQ1GsaK2m32hacfF7OvL8iYq92uF//NUm - mhxeh5Jso+/G9xAAqOBen92oFOrg3P+EkmIPRqvcgy4I3Tb22PaB3tGP4+iXpalc - ocMVDzVOLM2pwMwnffnecGv3UotJWdz/80CG8juuwOLXOIsOVAdEbKIXGknwLGyZ - V04+nBQLm+qKhLFigakx43Rmte7Fn3tydlJ+61DHBYtUF8/QRPw/1YN5ybXuZrTj - ci6zUfHxuUczlr8GQhXDy95yvtGlqsGB1roIiCB4Ld7PV3S3YK8+4AjuNFqMQ/+K - KNMsGWFzkXf8NPLe/AneVARsX0f2bV0YYcHYWQCZjrEyKrMcNYuK6uj4Z1Xmwn7f - 0gE3kjTPuG562jpHHElWHcZI40uNnYf0DBRyTQWLGqOuAVCDrg8+/gITBW98M3jd - OuPu5RQmtIkF9BFDYl3pdbzJ/4j+StAnRuZMjSSi17okRHFJEL3FZ2LdBim073ra - BCU+tufFOQUtR47mOuIjmRDI0GXjGcs1BcovaVViGisjwB0CSSLgnYIjZH6xsrA1 - ICc0bSNBvYpXewPGDQ8AO1k4lNtedDOY/sXr4o9xZVic5eOA740PPrEozpftV1wK - UBxa9dXTy9qaZhcw/x38zVyJTLg4I5sOmG24u/ZPqOblEKyQwFdU4gLyF5CRGwDy - 5LmGVc1bTDL8SQXkRG4L/DZVbqgBxBKNonkbnJV2VpjzecfA3O8rHS+uAlQ= - =hbUK + mDMEYigDUBYJKwYBBAHaRw8BAQdAPSmEMgZc23ouaPwdW7IF5Ej5H7VUGR6Um8N4 + SAW92i60I01pZ3VlbCBOb2d1ZWlyYSA8bWVAbm9ndWVpcmEuY29kZXM+iJYEExYI + AD4WIQTmfmRZELec2wocoaqNKugPeLO16wUCYigDUAIbAwUJAMo64AULCQgHAgYV + CgkICwIEFgIDAQIeAQIXgAAKCRCNKugPeLO169kWAQCwd4E0WBB98/1IdMrFAFPZ + 7zGrIeVGdUD4zg9E8ssLwgEAwYIhnZI9bDaMUit2Fat7PEiqYIiNd1vKev3vO2wm + Iw+4OARiKANQEgorBgEEAZdVAQUBAQdAMqfyInidoScxFQAXOdrmpEJW0auO7D+4 + UnCZr87CugUDAQgHiH4EGBYIACYWIQTmfmRZELec2wocoaqNKugPeLO16wUCYigD + UAIbDAUJAMo64AAKCRCNKugPeLO165kGAP4if46/uChEaEwtlQO5fPbMwLmnAKyw + K2ImmP3ksxhh1wEA8R0fD0etl5VfcG2lp7h1e/VckbVaQzYwpyETHku39gM= + =9azb -----END PGP PUBLIC KEY BLOCK----- -You may use [this tool](https://sela.io/pgp-en/) to encrypt your message. +You may use [this tool](https://pgptool.org/) to encrypt and sign your message. diff --git a/app/Application.php b/app/Application.php index 5aa958a..4b15b06 100755 --- a/app/Application.php +++ b/app/Application.php @@ -33,11 +33,6 @@ class Application extends Model ]; - public function oneoffApplicant() - { - return $this->hasOne('App\OneoffApplicant', 'application_id', 'id'); - } - public function user() { return $this->belongsTo('App\User', 'applicantUserID', 'id'); @@ -70,11 +65,4 @@ class Application extends Model ]); } - - public function isOneoff() - { - return $this->user->id == 1; // ID 1 is always the ghost - } - - } diff --git a/app/Ban.php b/app/Ban.php index f4c2dfb..e5044cf 100755 --- a/app/Ban.php +++ b/app/Ban.php @@ -36,7 +36,7 @@ class Ban extends Model ]; public $dates = [ - 'suspendedUntil', + 'bannedUntil', ]; public function user() diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index a447643..fa03bf4 100755 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -22,6 +22,7 @@ namespace App\Console; use App\Jobs\ProcessDueSuspensions; +use App\Jobs\ProcessExpiredAbsences; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Foundation\Console\Kernel as ConsoleKernel; @@ -54,6 +55,11 @@ class Kernel extends ConsoleKernel ->daily(); // Production value: Every day // Development value: Every minute + + $schedule->job(new ProcessExpiredAbsences) + ->daily(); + // Production value: Every day + // Development value: Every minute } /** diff --git a/app/Exceptions/AccountNotLinkedException.php b/app/Exceptions/AccountNotLinkedException.php new file mode 100755 index 0000000..a3459f9 --- /dev/null +++ b/app/Exceptions/AccountNotLinkedException.php @@ -0,0 +1,10 @@ +. + */ + +namespace App\Facades; + +use Illuminate\Support\Facades\Facade; + +class Discord extends Facade +{ + protected static function getFacadeAccessor() + { + return 'discordServiceFacade'; + } +} diff --git a/app/Form.php b/app/Form.php index 80220fc..59afbae 100755 --- a/app/Form.php +++ b/app/Form.php @@ -40,6 +40,6 @@ class Form extends Model public function responses() { - return $this->belongsTo('App\Response', 'id', 'id'); + return $this->belongsTo('App\Response', 'id', 'responseFormID'); } } diff --git a/app/Helpers/Discord.php b/app/Helpers/Discord.php new file mode 100755 index 0000000..e4b0a1f --- /dev/null +++ b/app/Helpers/Discord.php @@ -0,0 +1,236 @@ +. + */ + + +namespace App\Helpers; + +use App\Exceptions\AccountNotLinkedException; +use App\User; +use Carbon\Carbon; +use Illuminate\Http\Client\RequestException; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\Http; + +// Small wrapper for the necessary sections of the Discord API; A library is overkill here +class Discord +{ + + /** + * The current working guild. Default is Home guild from app config + * @var string + */ + protected string $workingGuild; + + + /** + * Current user. + * + * @var User The user all methods will affect. + */ + protected User $user; + + + + public function __construct() { + if (isset($this->workingGuild)) { + $this->setWorkingGuild(config('services.discord.home_guild')); + } + } + + + /** + * Sets the working guild + * + * @param string $workingGuild + * @return Discord + */ + public function setWorkingGuild(string $workingGuild): Discord + { + $this->workingGuild = $workingGuild; + return $this; + } + + /** + * Sets the current user, upon validation + * + * @param User $user + * @return Discord + * @throws AccountNotLinkedException + */ + public function setUser(User $user): Discord + { + if ($user->hasDiscordConnection()) { + $this->user = $user; + return $this; + } + + throw new AccountNotLinkedException('Specified website user has not linked their Discord account yet.'); + } + + /** + * Obtains the current user's authentication info. Caches the data for the access_token's TTL, thus + * preventing unnecessary API requests. + * + * @return object Current user's authentication info (has no sensitive fields) + * @throws RequestException + */ + public function getAuthorizationInfo(): object { + + if (Cache::has($this->user->discord_user_id)) { + return unserialize(Cache::get($this->user->discord_user_id)); + } + else { + $authInfo = (object) Http::withToken($this->user->discord_token) + ->get(config('services.discord.base_url') . '/oauth2/@me') + ->throw() + ->json(); + + Cache::put($this->user->discord_user_id, serialize($authInfo), Carbon::parse($authInfo->expires)); + + return $authInfo; + } + } + + + /** + * Checks if the user's token is close to expiring. + * Tokens should be refreshed the day before they expire. + * + * @return bool Whether the user's token needs to be refreshed + * @throws RequestException + */ + public function needsRefresh(): bool { + return Carbon::parse($this->getAuthorizationInfo()->expires)->diffInDays() == 1; + } + + + + public function exchangeRefreshToken() { + + } + + + + /** + * Adds current working user to current working guild. Bot must be member of target guild, and account must be linked + * + * @return object|bool A GuildMember object; false if member is already in guild + * @throws RequestException Any client and server errors + * @see https://discord.com/developers/docs/resources/guild#guild-member-object + */ + public function addGuildMember(): object|bool + { + $params = [ + 'access_token' => $this->user->discord_token + ]; + + $member = Http::withBody(json_encode($params), 'application/json') + ->withHeaders([ + 'Authorization' => 'Bot ' . config('services.discord.token') + ])->put(config('services.discord.base_url') . "/guilds/{$this->workingGuild}/members/{$this->user->discord_user_id}") + ->throw(); + + if ($member->successful() && $member->status() == 204) { + return false; + } else { + return (object) $member->json(); + } + } + + + /** + * Bans a specified user from the guild. + * May be called from the suspension service optionally by the banning user + * + * @param string $reason The reason to supply Discord with + * @return void Nothing on success + * @throws RequestException + * @throws AccountNotLinkedException + */ + public function addGuildBan(string $reason): void { + Http::withHeaders([ + 'Authorization' => 'Bot ' . config('services.discord.token'), + 'X-Audit-Log-Reason' => $reason + ])->put(config('services.discord.base_url') . "/guilds/{$this->workingGuild}/bans/{$this->user->discord_user_id}") + ->throw(); + + throw new AccountNotLinkedException('Specified website user has not linked their Discord account yet.'); + } + + /** + * @param string $reason The removal reason to provide Discord with (e.g. ban expired) + * @return null|bool Null on unnan, false if user is not banned + * @throws RequestException + */ + public function removeGuildBan(string $reason): null|bool { + + if ($this->getGuildBan($this->user)) { + Http::withHeaders([ + 'Authorization' => 'Bot ' . config('services.discord.token'), + 'X-Audit-Log-Reason' => $reason + ])->delete(config('services.discord.base_url') . "/guilds/{$this->workingGuild}/bans/{$this->user->discord_user_id}") + ->throw(); + + return null; + } + return false; + } + + /** + * Gets (possible) ban for current user. + * + * @return object|bool Ban object if user is banned. Null + * @throws RequestException + * @see https://discord.com/developers/docs/resources/guild#ban-object + */ + public function getGuildBan(): object|bool + { + $ban = Http::withHeaders([ + 'Authorization' => 'Bot ' . config('services.discord.token') + ])->get(config('services.discord.base_url') . "/guilds/{$this->workingGuild}/bans/{$this->user->discord_user_id}"); + + if ($ban->status() == 404) { + return false; + } + + return ($ban->successful()) ? (object) $ban->json() : $ban->throwIf($ban->status() !== 404); + } + + + /** + * Retrieves list of Role objects + * @see https://discord.com/developers/docs/topics/permissions#role-object + * @return array List of role objects + * + * @throws RequestException + */ + public function getGuildRoles(): array { + return Http::withHeaders([ + 'Authorization' => 'Bot ' . config('services.discord.token') + ])->get(config('services.discord.base_url') . "/guilds/{$this->workingGuild}/roles") + ->throw() + ->json(); + } + +} diff --git a/app/Helpers/Options.php b/app/Helpers/Options.php index a1b35e8..1297a23 100755 --- a/app/Helpers/Options.php +++ b/app/Helpers/Options.php @@ -37,8 +37,9 @@ class Options /** * Returns an assortment of settings found in the mentioned category * - * @param $category The category + * @param string $category The category * @return Collection The settings in this category + * @throws EmptyOptionsException */ public function getCategory(string $category): Collection { diff --git a/app/Http/Controllers/AbsenceController.php b/app/Http/Controllers/AbsenceController.php index a8ee62f..1062867 100755 --- a/app/Http/Controllers/AbsenceController.php +++ b/app/Http/Controllers/AbsenceController.php @@ -6,7 +6,9 @@ use App\Absence; use App\Exceptions\AbsenceNotActionableException; use App\Http\Requests\StoreAbsenceRequest; use App\Http\Requests\UpdateAbsenceRequest; +use App\Services\AbsenceService; use App\User; +use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Carbon; @@ -15,28 +17,12 @@ use Illuminate\Support\Facades\Auth; class AbsenceController extends Controller { - /** - * Determines whether someone already has an active leave of absence request - * - * @param User $user The user to check - * @return bool Their status - */ - private function hasActiveRequest(Authenticatable $user): bool { + private AbsenceService $absenceService; - $absences = Absence::where('requesterID', $user->id)->get(); + public function __construct (AbsenceService $absenceService) { - foreach ($absences as $absence) { + $this->absenceService = $absenceService; - // Or we could adjust the query (using a model scope) to only return valid absences; - // If there are any, refuse to store more, but this approach also works - // A model scope that only returns cancelled, declined and ended absences could also be implemented for future use - if (in_array($absence->getRawOriginal('status'), ['PENDING', 'APPROVED'])) - { - return true; - } - } - - return false; } /** @@ -57,7 +43,7 @@ class AbsenceController extends Controller * Display a listing of absences belonging to the current user. * * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View - * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws AuthorizationException */ public function showUserAbsences() { @@ -76,14 +62,14 @@ class AbsenceController extends Controller /** * Show the form for creating a new absence request. * - * @return \Illuminate\Http\Response + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View */ public function create() { $this->authorize('create', Absence::class); return view('dashboard.absences.create') - ->with('activeRequest', $this->hasActiveRequest(Auth::user())); + ->with('activeRequest', $this->absenceService->hasActiveRequest(Auth::user())); } /** @@ -96,21 +82,13 @@ class AbsenceController extends Controller { $this->authorize('create', Absence::class); - if ($this->hasActiveRequest(Auth::user())) { + if ($this->absenceService->hasActiveRequest(Auth::user())) { return redirect() ->back() ->with('error', __('You already have an active request. Cancel it or let it expire first.')); } - - $absence = Absence::create([ - 'requesterID' => Auth::user()->id, - 'start' => $request->start_date, - 'predicted_end' => $request->predicted_end, - 'available_assist' => $request->available_assist == "on", - 'reason' => $request->reason, - 'status' => 'PENDING', - ]); + $absence = $this->absenceService->createAbsence(Auth::user(), $request); return redirect() ->to(route('absences.show', ['absence' => $absence->id])) @@ -120,7 +98,8 @@ class AbsenceController extends Controller /** * Display the specified absence request. * - * @param \App\Absence $absence + * @param \App\Absence $absence + * @throws AuthorizationException */ public function show(Absence $absence) { @@ -138,7 +117,7 @@ class AbsenceController extends Controller * * @param Absence $absence * @return RedirectResponse - * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws AuthorizationException */ public function approveAbsence(Absence $absence): RedirectResponse { @@ -146,7 +125,7 @@ class AbsenceController extends Controller try { - $absence->setApproved(); + $this->absenceService->approveAbsence($absence); } catch (AbsenceNotActionableException $notActionableException) { @@ -166,7 +145,7 @@ class AbsenceController extends Controller * * @param Absence $absence * @return RedirectResponse - * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws AuthorizationException */ public function declineAbsence(Absence $absence): RedirectResponse { @@ -174,7 +153,7 @@ class AbsenceController extends Controller try { - $absence->setDeclined(); + $this->absenceService->declineAbsence($absence); } catch (AbsenceNotActionableException $notActionableException) { return redirect() @@ -193,7 +172,7 @@ class AbsenceController extends Controller * * @param Absence $absence * @return \Illuminate\Http\RedirectResponse - * @throws \Illuminate\Auth\Access\AuthorizationException + * @throws AuthorizationException */ public function cancelAbsence(Absence $absence): \Illuminate\Http\RedirectResponse { @@ -201,7 +180,7 @@ class AbsenceController extends Controller try { - $absence->setCancelled(); + $this->absenceService->cancelAbsence($absence); } catch (AbsenceNotActionableException $notActionableException) { @@ -225,7 +204,7 @@ class AbsenceController extends Controller { $this->authorize('delete', $absence); - if ($absence->delete()) { + if ($this->absenceService->removeAbsence($absence)) { return redirect() ->to(route('absences.index')) ->with('success', __('Absence request deleted.')); diff --git a/app/Http/Controllers/ApplicationController.php b/app/Http/Controllers/ApplicationController.php index 822020b..c418c10 100755 --- a/app/Http/Controllers/ApplicationController.php +++ b/app/Http/Controllers/ApplicationController.php @@ -23,11 +23,15 @@ namespace App\Http\Controllers; use App\Application; use App\Exceptions\ApplicationNotFoundException; +use App\Exceptions\DiscordAccountRequiredException; +use App\Exceptions\IncompatibleAgeException; use App\Exceptions\IncompleteApplicationException; +use App\Exceptions\InvalidAgeException; use App\Exceptions\UnavailableApplicationException; use App\Exceptions\VacancyNotFoundException; use App\Facades\IP; use App\Services\ApplicationService; +use App\Vacancy; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; @@ -82,6 +86,13 @@ class ApplicationController extends Controller } + public function discordApply(Request $request, $vacancySlug) { + + $request->session()->put('discordApplicationRedirectedSlug', $vacancySlug); + return redirect(route('discordRedirect')); + + } + public function renderApplicationForm($vacancySlug) { try { @@ -91,25 +102,47 @@ class ApplicationController extends Controller return redirect() ->back() ->with('error', $ex->getMessage()); + + } catch (DiscordAccountRequiredException $e) { + \Log::info('Redirecting user: ' . $e->getMessage(), [ + 'user' => Auth::user()->email + ]); + + request()->session()->put('discordApplicationRedirectedSlug', $vacancySlug); + return redirect(route('discordRedirect')); + } catch (IncompatibleAgeException $e) { + + return redirect() + ->to(route('dashboard')) + ->with('error', $e->getMessage()); + + } catch (InvalidAgeException $e) { + + return view('dashboard.application-rendering.add-age'); } } public function saveApplicationAnswers(Request $request, $vacancySlug) { - try { + if (Auth::user()->isEligible()) { + try { + $this->applicationService->fillForm(Auth::user(), $request->all(), $vacancySlug); - $this->applicationService->fillForm(Auth::user(), $request->all(), $vacancySlug); + } catch (VacancyNotFoundException | IncompleteApplicationException | UnavailableApplicationException $e) { - } catch (VacancyNotFoundException | IncompleteApplicationException | UnavailableApplicationException $e) { + return redirect() + ->back() + ->with('error', $e->getMessage()); + } return redirect() - ->back() - ->with('error', $e->getMessage()); + ->to(route('showUserApps')) + ->with('success', __('Thank you! Your application has been processed and our team will get to it shortly.')); } return redirect() ->to(route('showUserApps')) - ->with('success', __('Thank you! Your application has been processed and our team will get to it shortly.')); + ->with('error', __('Your account is not eligible to submit a new application.')); } public function updateApplicationStatus(Request $request, Application $application, $newStatus) diff --git a/app/Http/Controllers/Auth/DiscordController.php b/app/Http/Controllers/Auth/DiscordController.php new file mode 100755 index 0000000..ddaf82c --- /dev/null +++ b/app/Http/Controllers/Auth/DiscordController.php @@ -0,0 +1,101 @@ +. + */ + +namespace App\Http\Controllers\Auth; + +use App\Facades\Discord; +use App\Facades\Options; +use App\Http\Controllers\Controller; +use App\User; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; +use Laravel\Socialite\Facades\Socialite; +use Laravel\Socialite\Two\InvalidStateException; + +class DiscordController extends Controller +{ + + + public function discordRedirect() { + return Socialite::driver('discord') + ->scopes(['email', 'guilds.join', 'guilds.members.read', 'guilds']) + ->redirect(); + } + + public function discordCallback() { + + try { + + $discordUser = Socialite::driver('discord')->user(); + + } catch (InvalidStateException $stateException) { + Log::warning('Invalid state for social authentication: ', [ + 'message' => $stateException->getMessage(), + 'ua' => request()->userAgent(), + 'ip' => request()->ip() + ]); + return redirect(route('discordRedirect')); + } + + $appUser = User::where('email', $discordUser->getEmail())->first(); + + if ($appUser) { + + $appUser->discord_token = $discordUser->token; + $appUser->discord_refresh_token = $discordUser->refreshToken; + $appUser->discord_user_id = $discordUser->getId(); + $appUser->discord_pfp = $discordUser->getAvatar(); + $appUser->save(); + + Auth::login($appUser, true); + + } else { + + $oAuthUser = User::create([ + 'uuid' => null, + 'name' => $discordUser->getName(), + 'email' => $discordUser->getEmail(), + 'email_verified_at' => now(), // verify the account since it came from a trusted provider + 'username' => $discordUser->getNickname(), + 'currentIp' => \request()->ip(), + 'registrationIp' => request()->ip(), + 'discord_user_id' => $discordUser->getId(), + 'discord_pfp' => $discordUser->getAvatar(), + 'discord_token' => $discordUser->token, + 'discord_refresh_token' => $discordUser->refreshToken + ]); + + $oAuthUser->assignRole('user'); + + Auth::login($oAuthUser, true); + } + + if (session()->has('discordApplicationRedirectedSlug')) { + return redirect(route('renderApplicationForm', ['vacancySlug' => session()->pull('discordApplicationRedirectedSlug')])); + } + + return redirect() + ->route('dashboard'); + } + +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index 15dc202..b5fd5d6 100755 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -26,8 +26,11 @@ use App\Services\AccountSuspensionService; use App\User; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Log; use App\Facades\IP; +use Illuminate\Support\Str; use Laravel\Socialite\Facades\Socialite; class LoginController extends Controller @@ -76,6 +79,14 @@ class LoginController extends Controller $isLocked = $service->isLocked($user); if ($isBanned || $isLocked) { + + Log::alert('Restricted user attempting to login.', [ + 'ip' => $request->ip(), + 'email' => $user->email, + 'isBanned' => $isBanned, + 'isLocked' => $isLocked + ]); + return false; } else { return $this->originalAttemptLogin($request); @@ -94,17 +105,11 @@ class LoginController extends Controller 'prev' => $user->originalIP, 'new' => $request->ip() ]); - $user->originalIP = $request->ip(); + $user->currentIp = $request->ip(); $user->save(); } } } - public function discordRedirect() { - return Socialite::driver('discord')->redirect(); - } - public function discordCallback() { - // TODO; - } } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index e02398d..00d0a62 100755 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -23,11 +23,13 @@ namespace App\Http\Controllers\Auth; use App\Http\Controllers\Controller; use App\Profile; +use App\Services\AccountSuspensionService; use App\User; use App\Facades\Options; use App\Facades\IP; use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Validator; class RegisterController extends Controller @@ -62,19 +64,6 @@ class RegisterController extends Controller $this->middleware('guest'); } - public function showRegistrationForm() - { - $users = User::where('originalIP', \request()->ip())->get(); - - foreach ($users as $user) { - if ($user && $user->isBanned()) { - abort(403, 'You do not have permission to access this page.'); - } - } - - return view('auth.register'); - } - /** * Get a validator for an incoming registration request. * @@ -106,9 +95,14 @@ class RegisterController extends Controller 'uuid' => (Options::getOption('requireGameLicense') && Options::getOption('currentGame') == 'MINECRAFT') ? ['required', 'string', 'unique:users', 'min:32', 'max:32'] : ['nullable', 'string'], 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'], + 'dob' => ['required', 'string', 'date_format:Y-m-d', 'before:-13 years'], + 'acceptTerms' => ['required', 'accepted'], 'password' => $password, ], [ - 'uuid.required' => 'Please enter a valid (and Premium) Minecraft username! We do not support cracked users.', + 'dob.before' => __('You must be 13 years of age or older in order to sign up for an account.'), + 'dob.required' => __('Please enter your date of birth.'), + 'uuid.required' => __('Please enter a valid (and Premium) Minecraft username! We do not support cracked users.'), + 'acceptTerms.required' => __('Please accept the Community Guidelines, Terms of Service and Privacy Policy to continue.') ]); } @@ -120,12 +114,16 @@ class RegisterController extends Controller */ protected function create(array $data) { + $ip = IP::shouldCollect() ? request()->ip() : '0.0.0.0'; + $user = User::create([ 'uuid' => $data['uuid'] ?? "disabled", 'name' => $data['name'], 'email' => $data['email'], 'password' => Hash::make($data['password']), - 'originalIP' => IP::shouldCollect() ? request()->ip() : '0.0.0.0', + 'registrationIp' => $ip, + 'currentIp' => $ip, + 'dob' => $data['dob'] ]); $user->assignRole('user'); diff --git a/app/Http/Controllers/BanController.php b/app/Http/Controllers/BanController.php deleted file mode 100755 index a299a67..0000000 --- a/app/Http/Controllers/BanController.php +++ /dev/null @@ -1,88 +0,0 @@ -. - */ - -namespace App\Http\Controllers; - -use App\Ban; -use App\Events\UserBannedEvent; -use App\Http\Requests\BanUserRequest; -use App\Services\AccountSuspensionService; -use App\User; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; - -class BanController extends Controller -{ - - protected $suspensionService; - - public function __construct(AccountSuspensionService $suspensionService) - { - // Inject the service via DI - $this->suspensionService = $suspensionService; - } - - public function insert(BanUserRequest $request, User $user) - { - if (config('demo.is_enabled')) { - return redirect() - ->back() - ->with('error', __('This feature is disabled')); - } - - $this->authorize('create', [Ban::class, $user]); - - - if (!$this->suspensionService->isSuspended($user)) { - - $this->suspensionService->suspend($request->reason, $request->duration, $user, $request->suspensionType); - $request->session()->flash('success', __('Account suspended.')); - - } else { - - $request->session()->flash('error', __('Account already suspended!')); - } - - return redirect()->back(); - } - - public function delete(Request $request, User $user) - { - if (config('demo.is_enabled')) { - return redirect() - ->back() - ->with('error', __('This feature is disabled')); - } - - $this->authorize('delete', $user->bans); - - if ($this->suspensionService->isSuspended($user)) { - - $this->suspensionService->unsuspend($user); - $request->session()->flash('success', __('Account unsuspended successfully!')); - - } else { - $request->session()->flash('error', __('This account isn\'t suspended!')); - } - - return redirect()->back(); - } -} diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php deleted file mode 100755 index 92961a6..0000000 --- a/app/Http/Controllers/ContactController.php +++ /dev/null @@ -1,64 +0,0 @@ -. - */ - -namespace App\Http\Controllers; - -use App\Exceptions\FailedCaptchaException; -use App\Http\Requests\HomeContactRequest; -use App\Notifications\NewContact; -use App\Services\ContactService; -use App\User; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Http; - -class ContactController extends Controller -{ - protected $users; - - private $contactService; - - public function __construct(User $users, ContactService $contactService) - { - $this->contactService = $contactService; - $this->users = $users; - } - - public function create(HomeContactRequest $request) - { - try { - - $email = $request->email; - $msg = $request->msg; - $challenge = $request->input('captcha'); - - $this->contactService->sendMessage($request->ip(), $msg, $email, $challenge); - - return redirect() - ->back() - ->with('success',__('Message sent successfully! We usually respond within 48 hours.')); - - } catch (FailedCaptchaException $ex) { - return redirect() - ->back() - ->with('error', $ex->getMessage()); - } - } -} diff --git a/app/Http/Controllers/DevToolsController.php b/app/Http/Controllers/DevToolsController.php index 666366d..582a09a 100755 --- a/app/Http/Controllers/DevToolsController.php +++ b/app/Http/Controllers/DevToolsController.php @@ -24,10 +24,12 @@ namespace App\Http\Controllers; use App\Application; use App\Events\ApplicationApprovedEvent; use App\Events\ApplicationDeniedEvent; +use App\Services\AbsenceService; use App\Services\AccountSuspensionService; use Illuminate\Http\Request; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; class DevToolsController extends Controller { @@ -105,4 +107,16 @@ class DevToolsController extends Controller ->with('error', __('There were no expired suspensions (or no suspensions at all) to purge.')); } + + public function endAbsencesNow(AbsenceService $service) + { + $this->singleAuthorise(); + + $service->endExpired(); + Log::alert('(absence cleaner) Forcefully started absence expiration check!'); + + return redirect() + ->back() + ->with('success', 'Cleaned up expired absences.'); + } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index f79d199..9293dc9 100755 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -41,11 +41,4 @@ class HomeController extends Controller return view('home') ->with('positions', $positions); } - - public function pageGiveaway() - { - - return view('giveaway'); - - } } diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index d77b028..767b7f3 100755 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -21,8 +21,12 @@ namespace App\Http\Controllers; +use App\Exceptions\ProfileAlreadyExistsException; +use App\Exceptions\ProfileCreationFailedException; +use App\Exceptions\ProfileNotFoundException; use App\Facades\IP; use App\Http\Requests\ProfileSave; +use App\Services\AccountSuspensionService; use App\Services\ProfileService; use App\User; use Carbon\Carbon; @@ -32,18 +36,12 @@ use Spatie\Permission\Models\Role; class ProfileController extends Controller { - private $profileService; + private ProfileService $profileService; public function __construct(ProfileService $profileService) { $this->profileService = $profileService; } - public function index() - { - return view('dashboard.user.directory') - ->with('users', User::with('profile', 'bans')->paginate(9)); - } - public function showProfile() { // TODO: Come up with cleaner social media solution, e.g. social media object @@ -60,26 +58,23 @@ class ProfileController extends Controller ]); } - public function showSingleProfile(User $user) + public function showSingleProfile(AccountSuspensionService $accountSuspensionService, User $user) { + + if (is_null($user->profile)) { + + return redirect() + ->back() + ->with('error', "This user doesn't have a profile."); + + } + $socialMediaProfiles = json_decode($user->profile->socialLinks, true); $createdDate = Carbon::parse($user->created_at); - $systemRoles = Role::all()->pluck('name')->all(); - $userRoles = $user->roles->pluck('name')->all(); - - $roleList = []; - - foreach ($systemRoles as $role) { - if (in_array($role, $userRoles)) { - $roleList[$role] = true; - } else { - $roleList[$role] = false; - } - } $suspensionInfo = null; - if ($user->isBanned()) + if ($accountSuspensionService->isSuspended($user)) { $suspensionInfo = [ @@ -98,8 +93,7 @@ class ProfileController extends Controller 'insta' => $socialMediaProfiles['links']['insta'] ?? 'UpdateMe', 'discord' => $socialMediaProfiles['links']['discord'] ?? 'UpdateMe#12345', 'since' => $createdDate->englishMonth.' '.$createdDate->year, - 'ipInfo' => IP::lookup($user->originalIP), - 'roles' => $roleList, + 'ipInfo' => IP::lookup($user->currentIp), 'suspensionInfo' => $suspensionInfo ]); } else { @@ -114,4 +108,44 @@ class ProfileController extends Controller ->back() ->with('success', __('Profile updated.')); } + + + public function createProfile(Request $request) + { + + try { + $this->profileService->createProfile($request->user()); + } catch (\Exception $e) { + + return redirect() + ->back() + ->with('error', $e->getMessage()); + + } + + return redirect() + ->back() + ->with('success', __('Your profile has been created.')); + } + + + + public function deleteProfile(Request $request) + { + + try { + $this->profileService->deleteProfile($request->user()); + } catch (ProfileNotFoundException $e) { + + return redirect() + ->back() + ->with('error', $e->getMessage()); + + } + + return redirect() + ->back() + ->with('success', __('Profile deleted successfully.')); + + } } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 4afc2f3..b50b4fa 100755 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -22,22 +22,36 @@ namespace App\Http\Controllers; use App\Ban; +use App\Facades\IP; +use App\Facades\Options; use App\Http\Requests\Add2FASecretRequest; +use App\Http\Requests\AddDobRequest; +use App\Http\Requests\BanUserRequest; use App\Http\Requests\ChangeEmailRequest; use App\Http\Requests\ChangePasswordRequest; use App\Http\Requests\DeleteUserRequest; use App\Http\Requests\FlushSessionsRequest; use App\Http\Requests\Remove2FASecretRequest; +use App\Http\Requests\Reset2FASecretRequest; use App\Http\Requests\SearchPlayerRequest; +use App\Http\Requests\SetNewPasswordRequest; use App\Http\Requests\UpdateUserRequest; use App\Notifications\ChangedPassword; use App\Notifications\EmailChanged; +use App\Notifications\PasswordAdminResetNotification; +use App\Notifications\TwoFactorResetNotification; +use App\Services\AccountSuspensionService; +use App\Services\DiscordService; use App\Traits\DisablesFeatures; use App\Traits\HandlesAccountDeletion; use App\Traits\ReceivesAccountTokens; use App\User; use Google2FA; +use Illuminate\Contracts\Foundation\Application; +use Illuminate\Http\Client\RequestException; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Routing\Redirector; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Log; @@ -45,13 +59,20 @@ use Spatie\Permission\Models\Role; class UserController extends Controller { - use HandlesAccountDeletion; + use HandlesAccountDeletion, DisablesFeatures; + + /** + * Shows list of users + * + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View + * @throws \Illuminate\Auth\Access\AuthorizationException + */ public function showUsers() { $this->authorize('viewPlayers', User::class); - return view('dashboard.administration.players') + return view('dashboard.administration.users') ->with([ 'users' => User::with('roles')->paginate('6'), 'numUsers' => count(User::all()), @@ -59,6 +80,15 @@ class UserController extends Controller ]); } + + /** + * Searches for a player with the given search query. + * + * @deprecated Until Algolia implementation + * @param SearchPlayerRequest $request + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + * @throws \Illuminate\Auth\Access\AuthorizationException + */ public function showPlayersLike(SearchPlayerRequest $request) { $this->authorize('viewPlayers', User::class); @@ -72,7 +102,7 @@ class UserController extends Controller if (! $matchingUsers->isEmpty()) { $request->session()->flash('success', __('There were :usersCount user(s) matching your search.', ['usersCount' => $matchingUsers->count()])); - return view('dashboard.administration.players') + return view('dashboard.administration.users') ->with([ 'users' => $matchingUsers, 'numUsers' => count(User::all()), @@ -85,6 +115,16 @@ class UserController extends Controller } } + + /** + * Shows the user account's settings page + * + * @param Request $request + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View + * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException + * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException + * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException + */ public function showAccount(Request $request) { $QRCode = null; @@ -109,6 +149,58 @@ class UserController extends Controller ->with('twofaQRCode', $QRCode); } + + /** + * Show account management screen + * + * @param AccountSuspensionService $suspensionService + * @param Request $request + * @param User $user + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function showAcocuntManagement(AccountSuspensionService $suspensionService, Request $request, User $user) + { + + $this->authorize('adminEdit', $user); + + $systemRoles = Role::all()->pluck('name')->all(); + $userRoles = $user->roles->pluck('name')->all(); + + $roleList = []; + + foreach ($systemRoles as $role) { + if (in_array($role, $userRoles)) { + $roleList[$role] = true; + } else { + $roleList[$role] = false; + } + } + + return view('dashboard.user.manage') + ->with([ + 'user' => $user, + 'roles' => $roleList, + 'isVerified' => $user->isVerified(), + 'isLocked' => $suspensionService->isLocked($user), + 'isSuspended' => $suspensionService->isSuspended($user), + 'hasDiscord' => $user->hasDiscordConnection(), + 'hasPassword' => $user->hasPassword(), + 'requireLicense' => Options::getOption('requireGameLicense'), + 'suspensionReason' => $suspensionService->getSuspensionReason($user), + 'suspensionDuration' => $suspensionService->getSuspensionDuration($user), + 'has2FA' => $user->has2FA(), + 'applications' => $user->applications()->get() + ]); + } + + /** + * Log out other sessions for the current user + * + * @param FlushSessionsRequest $request + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\AuthenticationException + */ public function flushSessions(FlushSessionsRequest $request) { // TODO: Move all log calls to a listener, which binds to an event fired by each significant event, such as this one @@ -127,6 +219,14 @@ class UserController extends Controller return redirect()->back(); } + + + /** + * Change the current user's password + * + * @param ChangePasswordRequest $request + * @return \Illuminate\Http\RedirectResponse|void + */ public function changePassword(ChangePasswordRequest $request) { if (config('demo.is_enabled')) { @@ -155,13 +255,80 @@ class UserController extends Controller } } + + /** + * Sets a new password for the user. + * + * @param SetNewPasswordRequest $request + * @return Application|RedirectResponse|Redirector + */ + public function setPassword(SetNewPasswordRequest $request) { + + if (!Auth::user()->hasPassword()) { + + Auth::user()->password = Hash::make($request->newpass); + Auth::user()->save(); + + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect(route('login')); + } + + return redirect() + ->back() + ->with('error', __('Your account already has a password.')); + } + + + /** + * Sets a user's password and removes their discord information from storage + * + * @param User $user + * @param SetNewPasswordRequest $request + * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function unlinkDiscordAccount(Request $request, DiscordService $discordService) + { + if ($request->user()->hasPassword()) { + try { + $discordService->revokeAccountTokens(Auth::user()); + Log::warning('Revoking social account tokens, user initiated', [ + 'user' => Auth::user()->email + ]); + } catch (RequestException $requestException) { + + if ($requestException->getCode() == 401) { + return redirect(route('discordRedirect')); + } + + Log::error('Error while trying to revoke Discord credentials', [$requestException->getMessage()]); + return redirect() + ->back() + ->with('error', __('An unknown error ocurred. Please try again later.')); + } + + $request->session()->flash('success', __('Discord account unlinked successfully. Link it again by re-authorizing the app with the same account in the login screen, or through your account settings.')); + return redirect()->back(); + } + + return redirect() + ->back() + ->with('error', __('Please set a password for your account first before trying to unlink Discord.')); + + } + + + /** + * Change the current user's email address + * + * @param ChangeEmailRequest $request + * @return \Illuminate\Http\RedirectResponse + */ public function changeEmail(ChangeEmailRequest $request) { - if (config('demo.is_enabled')) { - return redirect() - ->back() - ->with('error', __('This feature is disabled')); - } + $this->disable(); $user = User::find(Auth::user()->id); @@ -184,14 +351,69 @@ class UserController extends Controller return redirect()->back(); } - public function delete(DeleteUserRequest $request, User $user) - { - if (config('demo.is_enabled')) { + + /** + * Removes the user's password and notifies them. + * + * @param User $user The user to remove the password for + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function forcePasswordReset(User $user) { + + $this->authorize('adminEdit', $user); + + if ($user->hasPassword()) { + $user->notify(new PasswordAdminResetNotification()); + + $user->password = null; + $user->save(); + + + Log::alert("Removed account password", [ + 'target' => $user, + 'actor' => Auth::user() + ]); + return redirect() ->back() - ->with('error', _('This feature is disabled')); + ->with('success', __('Account password removed.')); } + return redirect() + ->back() + ->with('error', __('This user doesn\'t have a password to reset.')); + } + + + /** + * Adds a user's date of birth if they don't have one. + * + * @param AddDobRequest $request + * @return RedirectResponse + */ + public function addDob(AddDobRequest $request) { + + Auth::user()->dob = $request->dob; + Auth::user()->save(); + + return redirect() + ->back(); + } + + + /** + * Delete the given user's account + * + * @param DeleteUserRequest $request + * @param User $user + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function delete(DeleteUserRequest $request, User $user) + { + $this->disable(); + $this->authorize('delete', $user); if ($request->confirmPrompt == 'DELETE ACCOUNT') { @@ -204,14 +426,19 @@ class UserController extends Controller return redirect()->route('registeredPlayerList'); } + + /** + * Update a given user's details + * + * @param UpdateUserRequest $request + * @param User $user + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ public function update(UpdateUserRequest $request, User $user) { - if (config('demo.is_enabled')) { - return redirect() - ->back() - ->with('error', __('This feature is disabled')); - } $this->authorize('adminEdit', $user); + $this->disable(); // Mass update would not be possible here without extra code, making route model binding useless $user->email = $request->email; @@ -243,6 +470,16 @@ class UserController extends Controller return redirect()->back(); } + + /** + * Generate and add a 2FA secret for the current user + * + * @param Add2FASecretRequest $request + * @return \Illuminate\Http\RedirectResponse + * @throws \PragmaRX\Google2FA\Exceptions\IncompatibleWithGoogleAuthenticatorException + * @throws \PragmaRX\Google2FA\Exceptions\InvalidCharactersException + * @throws \PragmaRX\Google2FA\Exceptions\SecretKeyTooShortException + */ public function add2FASecret(Add2FASecretRequest $request) { if (config('demo.is_enabled')) { @@ -285,6 +522,13 @@ class UserController extends Controller return redirect()->back(); } + + /** + * Remove the current user's two factor secret key + * + * @param Remove2FASecretRequest $request + * @return \Illuminate\Http\RedirectResponse + */ public function remove2FASecret(Remove2FASecretRequest $request) { Log::warning('SECURITY: Disabling two factor authentication (user initiated)', [ @@ -300,34 +544,94 @@ class UserController extends Controller return redirect()->back(); } - public function terminate(Request $request, User $user) - { - $this->authorize('terminate', User::class); - if (config('demo.is_enabled')) { + + /** + * Remove the given user's two factor secret key + * + * @param Reset2FASecretRequest $request + * @param User $user + * @return \Illuminate\Http\RedirectResponse + */ + public function reset2FASecret(Reset2FASecretRequest $request, User $user) { + // note: could invalidate other sessions for increased security + if ($user->has2FA()) { + Log::warning('SECURITY: Disabling two factor authentication (admin initiated)', [ + 'initiator' => $request->user()->email, + 'target' => $user->email, + 'ip' => $request->ip(), + ]); + + $user->twofa_secret = null; + $user->password = null; + $user->save(); + + $user->notify(new TwoFactorResetNotification()); + return redirect() ->back() - ->with('error', __('This feature is disabled')); + ->with('success', __('Two factor removed & user notified.')); } - // TODO: move logic to policy - if (! $user->isStaffMember() || $user->is(Auth::user())) { - $request->session()->flash('error', __('You cannot terminate this user.')); + return redirect() + ->back() + ->with('error', 'This user does not have two-factor authentication enabled.'); + } - return redirect()->back(); + /** + * Suspend the given user + * + * @param AccountSuspensionService $suspensionService + * @param BanUserRequest $request + * @param User $user + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function suspend(AccountSuspensionService $suspensionService, BanUserRequest $request, User $user) + { + $this->authorize('create', [Ban::class, $user]); + $this->disable(); + + if ($suspensionService->isSuspended($user)) + { + return redirect() + ->back() + ->with('error', __('Account already suspended.')); } - foreach ($user->roles as $role) { - if ($role->name == 'user') { - continue; - } - - $user->removeRole($role->name); + if ($request->suspensionType = "on") { + $suspensionService->suspend($user, $request->reason, $request->duration); + } + else { + $suspensionService->suspend($user, $request->reason); } - Log::info('User '.$user->name.' has just been demoted.'); - $request->session()->flash('success', __('User terminated successfully.')); - - //TODO: Dispatch event return redirect()->back(); } + + /** + * Unsuspend the given user + * + * @param AccountSuspensionService $suspensionService + * @param Request $request + * @param User $user + * @return \Illuminate\Http\RedirectResponse + * @throws \Illuminate\Auth\Access\AuthorizationException + */ + public function unsuspend(AccountSuspensionService $suspensionService, Request $request, User $user) + { + $this->authorize('delete', $user->bans); + $this->disable(); + + if ($suspensionService->isSuspended($user)) { + + $suspensionService->unsuspend($user); + $request->session()->flash('success', __('Account unsuspended successfully!')); + + } else { + $request->session()->flash('error', __('This account isn\'t suspended!')); + } + + return redirect()->back(); + } + } diff --git a/app/Http/Controllers/VacancyController.php b/app/Http/Controllers/VacancyController.php index 6cfc3f5..84d5870 100755 --- a/app/Http/Controllers/VacancyController.php +++ b/app/Http/Controllers/VacancyController.php @@ -70,6 +70,8 @@ class VacancyController extends Controller 'discordRoleID' => $request->discordRole, 'vacancyFormID' => $request->vacancyFormID, 'vacancyCount' => $request->vacancyCount, + 'requiresDiscord' => $request->requireDiscordAccount == 'on', + 'requiredAge' => $request->requiredAge ]); @@ -142,6 +144,8 @@ class VacancyController extends Controller $vacancy->vacancyFullDescription = $request->vacancyFullDescription; $vacancy->vacancyDescription = $request->vacancyDescription; $vacancy->vacancyCount = $request->vacancyCount; + $vacancy->requiresDiscord = $request->requireDiscordAccount == 'on'; + $vacancy->requiredAge = $request->requiredAge; $vacancy->save(); @@ -153,10 +157,18 @@ class VacancyController extends Controller public function delete(Request $request, Vacancy $vacancy) { $this->authorize('delete', $vacancy); - $vacancy->delete(); + + if ($vacancy->teams->isEmpty()) { + + $vacancy->delete(); + + return redirect() + ->back() + ->with('success', __('Vacancy deleted. All applications associated with it are now gone too.')); + } return redirect() ->back() - ->with('success', __('Vacancy deleted. All applications associated with it are now gone too.')); + ->with('error', __('Please detach any teams that may be using this vacancy first.')); } } diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 634033c..c0e0d83 100755 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -89,10 +89,10 @@ class Kernel extends HttpKernel '2fa' => \PragmaRX\Google2FALaravel\Middleware::class, 'passwordexpiration' => \App\Http\Middleware\PasswordExpirationMiddleware::class, 'passwordredirect' => \App\Http\Middleware\PasswordExpirationRedirectMiddleware::class, - 'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class, - 'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class, - 'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class, - 'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class, - 'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class, + 'localize' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRoutes::class, + 'localizationRedirect' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationRedirectFilter::class, + 'localeSessionRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleSessionRedirect::class, + 'localeCookieRedirect' => \Mcamara\LaravelLocalization\Middleware\LocaleCookieRedirect::class, + 'localeViewPath' => \Mcamara\LaravelLocalization\Middleware\LaravelLocalizationViewPath::class, ]; } diff --git a/app/Http/Middleware/ApplicationEligibility.php b/app/Http/Middleware/ApplicationEligibility.php index 13cb1da..c28efc4 100755 --- a/app/Http/Middleware/ApplicationEligibility.php +++ b/app/Http/Middleware/ApplicationEligibility.php @@ -22,8 +22,11 @@ namespace App\Http\Middleware; use App\Application; +use App\User; use Carbon\Carbon; use Closure; +use Exception; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\View; @@ -33,34 +36,42 @@ class ApplicationEligibility /** * Handle an incoming request. * - * @param \Illuminate\Http\Request $request - * @param \Closure $next + * @deprecated Deprecated in 0.9.0 + * @see User::isEligible() + * @param Request $request + * @param Closure $next * @return mixed - * @throws \Exception + * @throws Exception */ public function handle($request, Closure $next) { - $curtime = new Carbon(now()); + $eligible = false; + $daysRemaining = __('N/A'); if (Auth::check()) { - $applications = Application::where('applicantUserID', Auth::user()->id)->get(); - $eligible = true; - $daysRemaining = 0; + $lastApplication = Application::where('applicantUserID', Auth::user()->id)->latest()->first(); - if (! $applications->isEmpty()) { - foreach ($applications as $application) { - $appTime = Carbon::parse($application->created_at); - if ($appTime->isSameMonth($curtime)) { - Log::warning('Notice: Application ID '.$application->id.' was found to be in the same month as today\'s time, making the user '.Auth::user()->name.' ineligible for application'); - $eligible = false; - } - } + if (is_null($lastApplication)) { + View::share('isEligibleForApplication', true); + View::share('eligibilityDaysRemaining', 0); - $allowedTime = Carbon::parse($applications->last()->created_at)->addMonth(); - $daysRemaining = $allowedTime->diffInDays(now()); + return $next($request); } + $daysRemaining = $lastApplication->created_at->addMonth()->diffInDays(now()); + if ($lastApplication->created_at->diffInMonths(now()) > 1 && in_array($lastApplication->applicationStatus, ['DENIED', 'APPROVED'])) { + + $eligible = true; + } + + Log::debug('Perfomed application eligibility check', [ + 'eligible' => $eligible, + 'daysRemaining' => $daysRemaining, + 'ipAddress' => Auth::user()->originalIP, + 'checkUserID' => Auth::user()->id + ]); + View::share('isEligibleForApplication', $eligible); View::share('eligibilityDaysRemaining', $daysRemaining); } diff --git a/app/Http/Middleware/Bancheck.php b/app/Http/Middleware/Bancheck.php index 6c8a8ef..9e17e2c 100755 --- a/app/Http/Middleware/Bancheck.php +++ b/app/Http/Middleware/Bancheck.php @@ -21,12 +21,21 @@ namespace App\Http\Middleware; +use App\Services\AccountSuspensionService; use Closure; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\View; class Bancheck { + private $suspensionService; + + + public function __construct(AccountSuspensionService $suspensionService) { + $this->suspensionService = $suspensionService; + } + + /** * Handle an incoming request. * @@ -37,11 +46,11 @@ class Bancheck public function handle($request, Closure $next) { $userIP = $request->ip(); - $anonymousUser = User::where('ipAddress', $userIP)->get(); + $anonymousUser = User::where('currentIp', $userIP)->get(); - if (Auth::check() && Auth::user()->isBanned()) { + if (Auth::check() && $this->suspensionService->isSuspended($anonymousUser)) { View::share('isBanned', true); - } elseif (! $anonymousUser->isEmpty() && User::find($anonymousUser->id)->isBanned()) { + } elseif (! $anonymousUser->isEmpty() && $this->suspensionService->isSuspended(User::find($anonymousUser->id))) { View::share('isBanned', true); } else { View::share('isBanned', false); diff --git a/app/Http/Middleware/ForceLogoutMiddleware.php b/app/Http/Middleware/ForceLogoutMiddleware.php index b3b2baa..a8b23fa 100755 --- a/app/Http/Middleware/ForceLogoutMiddleware.php +++ b/app/Http/Middleware/ForceLogoutMiddleware.php @@ -21,6 +21,7 @@ namespace App\Http\Middleware; +use App\Services\AccountSuspensionService; use Closure; use Illuminate\Support\Facades\Auth; @@ -35,10 +36,10 @@ class ForceLogoutMiddleware */ public function handle($request, Closure $next) { - if (Auth::user()->isBanned()) { - Auth::logout(); - $request->session()->flash('error', __('Your account is suspended. You will not be able to login or register until the suspension is lifted.')); + if ((new AccountSuspensionService())->isSuspended(Auth::user())) { + Auth::logout(); + $request->session()->flash('error', __('Your account is suspended. If you think this was a mistake, please contact an admin.')); return redirect('/'); } diff --git a/app/Http/Requests/AddDobRequest.php b/app/Http/Requests/AddDobRequest.php new file mode 100644 index 0000000..65e65f1 --- /dev/null +++ b/app/Http/Requests/AddDobRequest.php @@ -0,0 +1,25 @@ + 'required|string|date_format:Y-m-d|before:-13 years', + ]; + } + + public function authorize(): bool + { + if (is_null(Auth::user()->dob)) { + return true; + } + + return false; + } +} diff --git a/app/Http/Requests/AdminPasswordResetRequest.php b/app/Http/Requests/AdminPasswordResetRequest.php new file mode 100755 index 0000000..8012e1f --- /dev/null +++ b/app/Http/Requests/AdminPasswordResetRequest.php @@ -0,0 +1,28 @@ +has2FA()) { + return [ + 'currentPassword' => 'required|current_password:web', + 'otp' => 'required|integer|max:6', + ]; + } + + return [ + 'currentPassword' => 'required|current_password:web', + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/app/Http/Requests/Remove2FASecretRequest.php b/app/Http/Requests/Remove2FASecretRequest.php index 136d283..ee9652b 100755 --- a/app/Http/Requests/Remove2FASecretRequest.php +++ b/app/Http/Requests/Remove2FASecretRequest.php @@ -44,7 +44,6 @@ class Remove2FASecretRequest extends FormRequest { return [ 'currentPassword' => 'required|current_password', - 'consent' => 'required|accepted', ]; } } diff --git a/app/Http/Requests/Reset2FASecretRequest.php b/app/Http/Requests/Reset2FASecretRequest.php new file mode 100644 index 0000000..ac416ca --- /dev/null +++ b/app/Http/Requests/Reset2FASecretRequest.php @@ -0,0 +1,20 @@ + 'required|current_password', + ]; + } + + public function authorize(): bool + { + return true; + } +} diff --git a/app/Http/Requests/HomeContactRequest.php b/app/Http/Requests/SetNewPasswordRequest.php similarity index 65% rename from app/Http/Requests/HomeContactRequest.php rename to app/Http/Requests/SetNewPasswordRequest.php index d358dce..2baadc8 100644 --- a/app/Http/Requests/HomeContactRequest.php +++ b/app/Http/Requests/SetNewPasswordRequest.php @@ -4,7 +4,7 @@ namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; -class HomeContactRequest extends FormRequest +class SetNewPasswordRequest extends FormRequest { /** * Determine if the user is authorized to make this request. @@ -13,7 +13,11 @@ class HomeContactRequest extends FormRequest */ public function authorize() { - return true; + if (\Auth::user()->hasDiscordConnection()) { + return true; + } + + return false; } /** @@ -24,9 +28,7 @@ class HomeContactRequest extends FormRequest public function rules() { return [ - 'email' => 'required|email', - 'msg' => 'required|string', - 'captcha' => 'required|string' + 'newpass' => 'required|string|min:10|confirmed', ]; } } diff --git a/app/Http/Requests/UpdateUserRequest.php b/app/Http/Requests/UpdateUserRequest.php index 709f013..018a2c6 100755 --- a/app/Http/Requests/UpdateUserRequest.php +++ b/app/Http/Requests/UpdateUserRequest.php @@ -46,7 +46,7 @@ class UpdateUserRequest extends FormRequest return [ 'email' => 'required|email', 'name' => 'required|string', - 'uuid' => 'required|max:32|min:32', + 'uuid' => 'nullable|max:32|min:32', 'roles' => 'required_without_all', ]; } diff --git a/app/Http/Requests/VacancyEditRequest.php b/app/Http/Requests/VacancyEditRequest.php index b9c4c3e..9c9544c 100755 --- a/app/Http/Requests/VacancyEditRequest.php +++ b/app/Http/Requests/VacancyEditRequest.php @@ -47,6 +47,8 @@ class VacancyEditRequest extends FormRequest 'vacancyDescription' => 'required|string', 'vacancyFullDescription' => 'nullable|string', 'vacancyCount' => 'required|integer|min:1', + 'requireDiscordAccount' => 'required|string', + 'requiredAge' => 'required|integer|numeric|min:13|max:100' ]; } } diff --git a/app/Http/Requests/VacancyRequest.php b/app/Http/Requests/VacancyRequest.php index 07e333c..2e7251d 100755 --- a/app/Http/Requests/VacancyRequest.php +++ b/app/Http/Requests/VacancyRequest.php @@ -25,6 +25,8 @@ use Illuminate\Foundation\Http\FormRequest; class VacancyRequest extends FormRequest { + public mixed $requiresDiscordAccount; + /** * Determine if the user is authorized to make this request. * @@ -46,10 +48,12 @@ class VacancyRequest extends FormRequest 'vacancyName' => 'required|string', 'vacancyDescription' => 'required|string', 'vacancyFullDescription' => 'nullable|string', - 'permissionGroup' => 'required|string', - 'discordRole' => 'required|string', + 'permissionGroup' => 'nullable|string', + 'discordRole' => 'nullable|string', 'vacancyCount' => 'required|integer', 'vacancyFormID' => 'required|integer', + 'requireDiscordAccount' => 'required|string', + 'requiredAge' => 'required|integer|numeric|min:13|max:100' ]; } } diff --git a/app/Jobs/ProcessAccountDelete.php b/app/Jobs/ProcessAccountDelete.php old mode 100644 new mode 100755 diff --git a/app/Jobs/ProcessExpiredAbsences.php b/app/Jobs/ProcessExpiredAbsences.php new file mode 100755 index 0000000..e953f81 --- /dev/null +++ b/app/Jobs/ProcessExpiredAbsences.php @@ -0,0 +1,39 @@ +endExpired(); + } +} diff --git a/app/Listeners/NewUser.php b/app/Listeners/NewUser.php new file mode 100755 index 0000000..5d3f228 --- /dev/null +++ b/app/Listeners/NewUser.php @@ -0,0 +1,30 @@ +user->name.' has just registered for an account.'); - foreach (User::all() as $user) { - if ($user->hasRole('admin')) { - $user->notify(new NewUser($event->user)); - } - } + User::whereHas('roles', function ($q) { + $q->where('name', 'admin'); + })->get()->each(function ($user, $key) use ($event) { + $user->notify(new NewUser($event->user)); + }); + } } diff --git a/app/Listeners/PromoteUser.php b/app/Listeners/PromoteUser.php index e53493b..93675cd 100755 --- a/app/Listeners/PromoteUser.php +++ b/app/Listeners/PromoteUser.php @@ -45,11 +45,15 @@ class PromoteUser */ public function handle(ApplicationApprovedEvent $event) { - Log::info('User '.$event->application->user->name . 'has just been promoted (application approved)'); + Log::info('User promoted automatically (application approved)', [ + 'user' => $event->application->user->name, + 'vacancy' => $event->application->response->vacancy->vacancyName, + 'role' => 'staff' + ]); $event->application->setStatus('APPROVED'); $event->application->response->vacancy->decrease(); - $event->application->user->assignRole('reviewer'); + $event->application->user->assignRole('staff'); $event->application->user->notify(new ApplicationApproved($event->application)); } diff --git a/app/Notifications/AbsenceRequestApproved.php b/app/Notifications/AbsenceRequestApproved.php new file mode 100755 index 0000000..4da1094 --- /dev/null +++ b/app/Notifications/AbsenceRequestApproved.php @@ -0,0 +1,68 @@ +absence = $absence; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->greeting('Hi ' . $notifiable->name . ',') + ->from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - absence request approved') + ->line("Your recent Leave of Absence request from {$this->absence->created_at} has just been approved by an admin.") + ->line('Your inactivity during the period you selected won\'t be counted. You will receive another email notification when your request ends, or if you decide to cancel it.') + ->action('View your request', url(route('absences.show', ['absence' => $this->absence->id]))) + ->salutation('The team at ' . config('app.name')); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/AbsenceRequestCancelled.php b/app/Notifications/AbsenceRequestCancelled.php new file mode 100755 index 0000000..7e3d27f --- /dev/null +++ b/app/Notifications/AbsenceRequestCancelled.php @@ -0,0 +1,68 @@ +absence = $absence; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->greeting('Hi ' . $notifiable->name . ',') + ->from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - absence request cancelled') + ->line("This notification confirms that your recent Leave of Absence from {$this->absence->created_at} has just been cancelled by you.") + ->line('Please note that any inactivity will be counted in our activity metrics. You may also make a new request if you wish.') + ->action('Send new request', url(route('absences.create'))) + ->salutation('The team at ' . config('app.name')); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/AbsenceRequestDeclined.php b/app/Notifications/AbsenceRequestDeclined.php new file mode 100755 index 0000000..3fd5da3 --- /dev/null +++ b/app/Notifications/AbsenceRequestDeclined.php @@ -0,0 +1,68 @@ +absence = $absence; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->greeting('Hi ' . $notifiable->name . ',') + ->from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - absence request declined') + ->line("Your recent Leave of Absence request from {$this->absence->created_at} has just been declined by an admin.") + ->line('Please note that any inactivity will be counted in our activity metrics. You may make a new request, but we recommend you ask your team lead regarding your declined request.') + ->action('Send new request', url(route('absences.create'))) + ->salutation('The team at ' . config('app.name')); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/AbsenceRequestEnded.php b/app/Notifications/AbsenceRequestEnded.php new file mode 100755 index 0000000..cbbf512 --- /dev/null +++ b/app/Notifications/AbsenceRequestEnded.php @@ -0,0 +1,68 @@ +absence = $absence; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->greeting('Hi ' . $notifiable->name . ',') + ->from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - absence request expired') + ->line("Your Leave of Absence request from {$this->absence->created_at} (until {$this->absence->predicted_end}) has expired today.") + ->line('Please note that any inactivity will be counted in our activity metrics. You may now make a new request if you still need more time.') + ->action('Send new request', url(route('absences.create'))) + ->salutation('The team at ' . config('app.name')); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/AccountDeleted.php b/app/Notifications/AccountDeleted.php old mode 100644 new mode 100755 diff --git a/app/Notifications/AccountLocked.php b/app/Notifications/AccountLocked.php old mode 100644 new mode 100755 diff --git a/app/Notifications/AccountUnlocked.php b/app/Notifications/AccountUnlocked.php old mode 100644 new mode 100755 diff --git a/app/Notifications/NewAbsenceRequest.php b/app/Notifications/NewAbsenceRequest.php new file mode 100755 index 0000000..51add0b --- /dev/null +++ b/app/Notifications/NewAbsenceRequest.php @@ -0,0 +1,68 @@ +absence = $absence; + } + + /** + * Get the notification's delivery channels. + * + * @param mixed $notifiable + * @return array + */ + public function via($notifiable) + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param mixed $notifiable + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail($notifiable) + { + return (new MailMessage) + ->greeting('Hi ' . $notifiable->name . ',') + ->from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - new absence request pending review') + ->line("A new absence request has just been submitted, scheduled to end {$this->absence->predicted_end}. Please review this request and take the appropriate action(s). The requester will be notified of your decision by email.") + ->line("You are receiving this email because you're a site admin.") + ->action('Review request', url(route('absences.show', ['absence' => $this->absence->id]))) + ->salutation('The team at ' . config('app.name')); + } + + /** + * Get the array representation of the notification. + * + * @param mixed $notifiable + * @return array + */ + public function toArray($notifiable) + { + return [ + // + ]; + } +} diff --git a/app/Notifications/PasswordAdminResetNotification.php b/app/Notifications/PasswordAdminResetNotification.php new file mode 100755 index 0000000..d9195e5 --- /dev/null +++ b/app/Notifications/PasswordAdminResetNotification.php @@ -0,0 +1,35 @@ +from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - account password invalidated') + ->markdown('mail.adminreset', ['name' => $notifiable->name]); + } + + public function toArray($notifiable): array + { + return []; + } +} diff --git a/app/Notifications/TwoFactorResetNotification.php b/app/Notifications/TwoFactorResetNotification.php new file mode 100644 index 0000000..78fcd1c --- /dev/null +++ b/app/Notifications/TwoFactorResetNotification.php @@ -0,0 +1,35 @@ +from(config('notification.sender.address'), config('notification.sender.name')) + ->subject(config('app.name').' - your second factor has been reset') + ->markdown('mail.two-factor-reset', ['name' => $notifiable->name]); + } + + public function toArray($notifiable): array + { + return []; + } +} diff --git a/app/Notifications/UserDeletedAccount.php b/app/Notifications/UserDeletedAccount.php old mode 100644 new mode 100755 diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php index 3f4130d..298ac07 100755 --- a/app/Observers/UserObserver.php +++ b/app/Observers/UserObserver.php @@ -21,7 +21,10 @@ namespace App\Observers; +use App\Exceptions\ProfileAlreadyExistsException; +use App\Exceptions\ProfileCreationFailedException; use App\Profile; +use App\Services\ProfileService; use App\User; use Illuminate\Support\Facades\Log; @@ -40,15 +43,24 @@ class UserObserver */ public function created(User $user) { - // TODO: Make sure that the profile is created, throw an exception if otherwise, or try to recreate the profile. - Profile::create([ + $profileService = new ProfileService(); - 'profileShortBio' => 'Write a one-liner about you here!', - 'profileAboutMe' => 'Tell us a bit about you.', - 'socialLinks' => '{}', - 'userID' => $user->id, - - ]); + try + { + $profileService->createProfile($user); + } + catch (ProfileAlreadyExistsException $exception) + { + Log::error('Attempting to create profile that already exists!', [ + 'trace' => $exception->getTrace() + ]); + } + catch (ProfileCreationFailedException $e) + { + Log::error('Failed creating a new profile!', [ + 'trace' => $e->getTrace() + ]); + } } /** diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 8061c19..3118a7f 100755 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -47,6 +47,7 @@ use App\Vote; use Illuminate\Auth\Notifications\VerifyEmail; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Notifications\Messages\MailMessage; +use Illuminate\Support\Facades\Gate; class AuthServiceProvider extends ServiceProvider { @@ -90,5 +91,17 @@ class AuthServiceProvider extends ServiceProvider ->salutation('The team at ' . config('app.name')); }); + + Gate::define('viewLogViewer', function (?User $user){ + return $user->hasPermissionTo('admin.developertools.use'); + }); + + Gate::define('downloadLogFile', function (User $user){ + return $user->hasPermissionTo('admin.developertools.use'); + }); + + Gate::define('deleteLogFile', function (User $user){ + return $user->hasPermissionTo('admin.developertools.use'); + }); } } diff --git a/app/Providers/DiscordOuthProvider.php b/app/Providers/DiscordOuthProvider.php new file mode 100755 index 0000000..0704604 --- /dev/null +++ b/app/Providers/DiscordOuthProvider.php @@ -0,0 +1,31 @@ + [ 'App\Listeners\OnUserBanned', ], + ]; /** diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 1c5f6b7..23eb156 100755 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -31,7 +31,7 @@ class RouteServiceProvider extends ServiceProvider * * @var string */ - public const HOME = '/home'; + public const HOME = '/dashboard'; /** * Define your route model bindings, pattern filters, etc. diff --git a/app/Services/AbsenceService.php b/app/Services/AbsenceService.php new file mode 100755 index 0000000..31c89f9 --- /dev/null +++ b/app/Services/AbsenceService.php @@ -0,0 +1,183 @@ +id)->get(); + + foreach ($absences as $absence) { + + // Or we could adjust the query (using a model scope) to only return valid absences; + // If there are any, refuse to store more, but this approach also works + // A model scope that only returns cancelled, declined and ended absences could also be implemented for future use + if (in_array($absence->getRawOriginal('status'), ['PENDING', 'APPROVED'])) + { + return true; + } + } + + return false; + } + + public function createAbsence(Authenticatable $requester, Request $request) + { + + $absence = Absence::create([ + 'requesterID' => $requester->id, + 'start' => $request->start_date, + 'predicted_end' => $request->predicted_end, + 'available_assist' => $request->available_assist == "on", + 'reason' => $request->reason, + 'status' => 'PENDING', + ]); + + foreach(User::role('admin')->get() as $admin) { + $admin->notify(new NewAbsenceRequest($absence)); + } + + Log::info('Processing new leave of absence request.', [ + 'requesting_user' => $requester->email, + 'absenceid' => $absence->id, + 'reason' => $request->reason + ]); + + return $absence; + } + + + /** + * Sets an absence as Approved. + * + * @param Absence $absence The absence to approve. + * @return Absence The approved absence. + * @throws AbsenceNotActionableException + */ + public function approveAbsence(Absence $absence) + { + Log::info('An absence request has just been approved.', [ + 'absenceid' => $absence->id, + 'reviewing_admim' => Auth::user()->email, + 'new_status' => 'APPROVED' + ]); + + return $absence + ->setApproved() + ->requester->notify(new AbsenceRequestApproved($absence)); + } + + + /** + * Sets an absence as Declined. + * + * @param Absence $absence The absence to decline. + * @return Absence The declined absence. + * @throws AbsenceNotActionableException + */ + public function declineAbsence(Absence $absence) + { + Log::warning('An absence request has just been declined.', [ + 'absenceid' => $absence->id, + 'reviewing_admim' => Auth::user()->email, + 'new_status' => 'DECLINED' + ]); + + return $absence + ->setDeclined() + ->requester->notify(new AbsenceRequestDeclined($absence)); + } + + + /** + * Sets an absence as Cancelled. + * + * @param Absence $absence The absence to cancel. + * @return Absence The cancelled absence. + * @throws AbsenceNotActionableException + */ + public function cancelAbsence(Absence $absence) + { + Log::warning('An absence request has just been cancelled (only cancellable by requester).', [ + 'absenceid' => $absence->id, + 'new_status' => 'CANCELLED' + ]); + + return $absence + ->setCancelled() + ->requester->notify(new AbsenceRequestCancelled($absence)); + } + + /** + * Sets an absence as Ended. + * + * @param Absence $absence + * @return bool + */ + public function endAbsence(Absence $absence) + { + Log::info('An absence request has just expired.', [ + 'absenceid' => $absence->id, + 'new_status' => 'ENDED' + ]); + + return $absence + ->setEnded() + ->requester->notify(new AbsenceRequestEnded($absence)); + } + + /** + * Removes an absence + * + * @param Absence $absence The absence to remove. + * @return bool Whether the absence was removed. + */ + public function removeAbsence(Absence $absence): bool + { + Log::alert('An absence request has just been removed.', [ + 'absence_details' => $absence, + 'reviewing_admim' => Auth::user()->email, + ]); + + return $absence->delete(); + } + + + public function endExpired() + { + foreach (Absence::all() as $absence) + { + if (!Carbon::parse($absence->predicted_end)->isFuture()) { + $this->endAbsence($absence); + } + } + } + + + +} + diff --git a/app/Services/AccountSuspensionService.php b/app/Services/AccountSuspensionService.php index ca3088b..4a9236a 100755 --- a/app/Services/AccountSuspensionService.php +++ b/app/Services/AccountSuspensionService.php @@ -16,18 +16,14 @@ class AccountSuspensionService /** * Suspends a user account, with given $reason. + * Permanent if no duration given. * - * This method will take the target user and add a suspension to the database, - * effectively banning the user from the app. Suspensions may be temporary or permanent. - * Suspensions also block registration attempts. - * - * @param string $reason Suspension reason. - * @param string $duration Duration. This is a timestamp. * @param User $target Who to suspend. - * @param string $type Permanent or temporary? + * @param string $reason Suspension reason. + * @param int|null $duration Duration in days * @return Ban The ban itself */ - public function suspend($reason, $duration, User $target, $type = "on"): Ban { + public function suspend(User $target, string $reason, int $duration = null): Ban { Log::alert("An user account has just been suspended.", [ 'taget_email' => $target->email, @@ -35,19 +31,17 @@ class AccountSuspensionService 'reason' => $reason ]); - if ($type == "on") { + if ($duration > 0) { $expiryDate = now()->addDays($duration); } - $ban = Ban::create([ + return Ban::create([ 'userID' => $target->id, 'reason' => $reason, - 'bannedUntil' => ($type == "on") ? $expiryDate->format('Y-m-d H:i:s') : null, + 'bannedUntil' => ($duration > 0) ? $expiryDate->format('Y-m-d H:i:s') : null, 'authorUserID' => Auth::user()->id, - 'isPermanent' => ($type == "off") ? true : false + 'isPermanent' => ($duration == 0) ? true : false ]); - - return $ban; } /** @@ -64,6 +58,16 @@ class AccountSuspensionService $user->bans->delete(); } + /** + * Checks whether a user is suspended + * + * @param User $user The user to check + * @return bool Whether the mentioned user is suspended + */ + public function isSuspended(User $user): bool { + return !is_null($user->bans); + } + @@ -107,19 +111,6 @@ class AccountSuspensionService return $user->save(); } - - - /** - * Checks whether a user is suspended - * - * @param User $user The user to check - * @return bool Whether the mentioned user is suspended - */ - public function isSuspended(User $user): bool { - return !is_null($user->bans); - } - - /** * Checks whether an account is locked * @@ -131,21 +122,21 @@ class AccountSuspensionService } /** - * Takes a suspension directly and makes it permanent. + * Retrieves the reason for the user's suspension. * - * @param Ban $ban The suspension to make permanent + * @param User $user The user account to check + * @return string|bool Reason for the suspension, false if not suspended */ - public function makePermanent(Ban $ban): void { + public function getSuspensionReason(User $user): string|bool { + return ($this->isSuspended($user)) ? $user->bans->reason : false; + } - Log::alert('A suspension has just been made permanent.', [ - 'target_email' => $ban->user->email - ]); - - $ban->bannedUntil = null; - $ban->isPermanent = true; - - $ban->save(); + public function getSuspensionDuration(User $user): string|null { + if ($this->isSuspended($user) && !is_null($user->bans->bannedUntil)) { + return $user->bans->bannedUntil->diffForHumans(); + } + return null; } /** diff --git a/app/Services/ApplicationService.php b/app/Services/ApplicationService.php index 27ef2e9..d5d02cd 100755 --- a/app/Services/ApplicationService.php +++ b/app/Services/ApplicationService.php @@ -3,7 +3,11 @@ namespace App\Services; +use App\Exceptions\DiscordAccountRequiredException; +use App\Exceptions\IncompatibleAgeException; +use App\Exceptions\InvalidAgeException; use App\Notifications\ApplicationConfirmed; +use Carbon\Carbon; use ContextAwareValidator; use App\Application; use App\Events\ApplicationDeniedEvent; @@ -22,12 +26,27 @@ use Illuminate\Support\Facades\Log; class ApplicationService { + /** + * @throws DiscordAccountRequiredException + * @throws IncompatibleAgeException + * @throws InvalidAgeException + */ public function renderForm($vacancySlug) { $vacancyWithForm = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get(); - $firstVacancy = $vacancyWithForm->first(); + if (is_null(Auth::user()->dob)) { + throw new InvalidAgeException("User must have added their age to apply for this vacancy."); + } elseif(Carbon::parse(Auth::user()->dob)->age < $firstVacancy->requiredAge) { + throw new IncompatibleAgeException("Sorry, you must be {$firstVacancy->requiredAge} or older to apply to {$firstVacancy->vacancyName}."); + } + + + if ($firstVacancy->requiresDiscord && !Auth::user()->hasDiscordConnection()) { + throw new DiscordAccountRequiredException('A discord account is required beyond this point.'); + } + if (!$vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN') { return view('dashboard.application-rendering.apply') ->with([ @@ -36,7 +55,7 @@ class ApplicationService ]); } else { - throw new ApplicationNotFoundException('The application you\'re looking for could not be found or it is currently unavailable.', 404); + throw new ApplicationNotFoundException(__('The application you\'re looking for could not be found or it is currently unavailable.'), 404); } } @@ -94,11 +113,12 @@ class ApplicationService 'applicant' => $applicant->name ]); - foreach (User::all() as $user) { - if ($user->hasRole('admin')) { - $user->notify((new NewApplicant($application, $vacancy->first()))); - } - } + User::whereHas('roles', function ($q) { + $q->where('name', 'admin'); + })->get()->each(function ($user, $key) use ($application, $vacancy) { + $user->notify((new NewApplicant($application, $vacancy->first()))); + }); + $application->user->notify(new ApplicationConfirmed($application)); return true; diff --git a/app/Services/DiscordService.php b/app/Services/DiscordService.php new file mode 100644 index 0000000..91e43d5 --- /dev/null +++ b/app/Services/DiscordService.php @@ -0,0 +1,42 @@ +post(config('services.discord.base_url') . '/oauth2/token/revoke', [ + 'client_id' => config('services.discord.client_id'), + 'client_secret' => config('services.discord.client_secret'), + 'token' => $user->discord_token, + ])->throw(); + + + + $user->discord_token = null; + $user->discord_user_id = null; + $user->discord_refresh_token = null; + $user->discord_pfp = null; + $user->save(); + + return $req->ok(); + } + + +} diff --git a/app/Services/FormManagementService.php b/app/Services/FormManagementService.php index a7e9ce0..0a59544 100755 --- a/app/Services/FormManagementService.php +++ b/app/Services/FormManagementService.php @@ -39,7 +39,7 @@ class FormManagementService $deletable = true; - if (! is_null($form) && ! is_null($form->vacancies) && $form->vacancies->count() !== 0 || ! is_null($form->responses)) { + if (! is_null($form->vacancies) && $form->vacancies->count() !== 0 || ! is_null($form->responses)) { $deletable = false; } diff --git a/app/Services/ProfileService.php b/app/Services/ProfileService.php index 438527b..3aa4b97 100755 --- a/app/Services/ProfileService.php +++ b/app/Services/ProfileService.php @@ -4,15 +4,56 @@ namespace App\Services; +use App\Exceptions\ProfileAlreadyExistsException; +use App\Exceptions\ProfileCreationFailedException; use App\Exceptions\ProfileNotFoundException; +use App\Profile; use App\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; class ProfileService { /** + * Creates a new profile for the specified $targetUser. + * + * @param User $targetUser The user to create the profile for. + * @return bool + * @throws ProfileAlreadyExistsException + * @throws ProfileCreationFailedException + */ + public function createProfile(User $targetUser): Profile { + + if (is_null($targetUser->profile)) { + + $profile = Profile::create([ + + 'profileShortBio' => 'Write a one-liner about you here!', + 'profileAboutMe' => 'Tell us a bit about you.', + 'socialLinks' => '{}', + 'userID' => $targetUser->id, + + ]); + + if (is_null($profile)) { + throw new ProfileCreationFailedException(__('Could not create profile! Please try again later.')); + } + + Log::info('Created profile for new user', [ + 'userid' => $targetUser->id + ]); + + return $profile; + } + + throw new ProfileAlreadyExistsException(__('Profile already exists!')); + } + + /** + * Updates the user's profile. + * * @throws ProfileNotFoundException */ public function updateProfile($userID, Request $request) { @@ -47,4 +88,26 @@ class ProfileService throw new ProfileNotFoundException("This profile does not exist."); } + /** + * Delete specified user's profile + * + * @param User $targetUser + * @return bool + * @throws ProfileNotFoundException + */ + public function deleteProfile(User $targetUser): bool + { + + if (!is_null($targetUser->profile)) { + + Log::alert('Deleted user profile', [ + 'userid' => $targetUser->id + ]); + + return $targetUser->profile->delete(); + } + + throw new ProfileNotFoundException(__('Attempting to delete non-existant profile!')); + } + } diff --git a/app/Traits/DisablesFeatures.php b/app/Traits/DisablesFeatures.php new file mode 100755 index 0000000..7c8ea26 --- /dev/null +++ b/app/Traits/DisablesFeatures.php @@ -0,0 +1,25 @@ +back() + ->with('error', __('This feature is disabled')); + } + return null; + } + +} diff --git a/app/User.php b/app/User.php index 2bca37a..c0c9210 100755 --- a/app/User.php +++ b/app/User.php @@ -24,15 +24,21 @@ namespace App; use App\Services\AccountSuspensionService; use App\Traits\HandlesAccountTokens; use Illuminate\Contracts\Auth\MustVerifyEmail; +use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Crypt; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\View; use Mpociot\Teamwork\Traits\UserHasTeams; use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable implements MustVerifyEmail { - use UserHasTeams, Notifiable, HasRoles; + use UserHasTeams, Notifiable, HasRoles, HasFactory; /** * The attributes that are mass assignable. @@ -40,7 +46,19 @@ class User extends Authenticatable implements MustVerifyEmail * @var array */ protected $fillable = [ - 'name', 'email', 'password', 'originalIP', 'username', 'uuid', 'dob', + 'name', + 'email', + 'password', + 'originalIP', + 'registrationIp', + 'username', + 'uuid', + 'dob', + 'email_verified_at', + 'currentIp', + 'discord_user_id', + 'discord_token', + 'discord_refresh_token' ]; /** @@ -49,7 +67,7 @@ class User extends Authenticatable implements MustVerifyEmail * @var array */ protected $hidden = [ - 'password', 'remember_token', + 'password', 'remember_token', 'discord_token', 'discord_refresh_token' ]; /** @@ -59,6 +77,8 @@ class User extends Authenticatable implements MustVerifyEmail */ protected $casts = [ 'email_verified_at' => 'datetime', + 'discord_token' => 'encrypted', + 'discord_refresh_token' => 'encrypted' ]; // RELATIONSHIPS @@ -71,6 +91,7 @@ class User extends Authenticatable implements MustVerifyEmail public function votes() { return $this->hasMany('App\Vote', 'userID', 'id'); + } public function profile() @@ -99,32 +120,52 @@ class User extends Authenticatable implements MustVerifyEmail } + public function isEligible(): bool { + $lastApplication = Application::where('applicantUserID', $this->getAttribute('id'))->latest()->first(); - // UTILITY LOGIC + if (is_null($lastApplication)) { + return true; + } - /** - * Checks if a user is banned. - * - * @deprecated This method is obsolete, as it has been replaced by the suspension service. - * @see AccountSuspensionService::isSuspended() - * - * @return bool Whether the user is banned - */ - public function isBanned(): bool - { - return ! $this->bans()->get()->isEmpty(); + if ($lastApplication->created_at->diffInMonths(now()) > 1 && in_array($lastApplication->applicationStatus, ['DENIED', 'APPROVED'])) { + return true; + } + + return false; } - public function isStaffMember() + + public function isVerified(): bool { + return !is_null($this->email_verified_at); + } + + /** + * Checks if user is staff + * + * @deprecated This method is being replaced by a better way of checking permissions, rather than checking for group name. + * @return bool + */ + public function isStaffMember(): bool { return $this->hasAnyRole('reviewer', 'admin', 'hiringManager'); } - public function has2FA() + /** + * Checks if user has 2fa enabled + * + * @return bool + */ + public function has2FA(): bool { return ! is_null($this->twofa_secret); } + /** + * Checks if user has team + * + * @param $team + * @return bool + */ public function hasTeam($team): bool { if ($team instanceof Team || is_int($team)) @@ -140,9 +181,23 @@ class User extends Authenticatable implements MustVerifyEmail } } - - public function routeNotificationForSlack($notification) - { - return config('slack.webhook.integrationURL'); + /** + * Check if user linked their Discord account + * + * @return bool + */ + public function hasDiscordConnection(): bool { + return !is_null($this->discord_token) && !is_null($this->discord_refresh_token); } + + + /** + * Check if user has a password + * + * @return bool + */ + public function hasPassword(): bool { + return !is_null($this->password); + } + } diff --git a/app/Vacancy.php b/app/Vacancy.php index a804662..4bf8d27 100755 --- a/app/Vacancy.php +++ b/app/Vacancy.php @@ -44,6 +44,8 @@ class Vacancy extends Model 'vacancyStatus', 'vacancySlug', 'team_id', + 'requiresDiscord', + 'requiredAge' ]; diff --git a/app/View/Components/AccountStatus.php b/app/View/Components/AccountStatus.php index 0c6bf6f..1180ce6 100755 --- a/app/View/Components/AccountStatus.php +++ b/app/View/Components/AccountStatus.php @@ -7,16 +7,27 @@ use Illuminate\View\Component; class AccountStatus extends Component { - public $user; + public bool + $isVerified, + $isSuspended, + $isLocked, + $has2FA, + $hasDiscord, + $hasPassword; /** * Create a new component instance. * * @return void */ - public function __construct($userId) + public function __construct($isVerified, $isSuspended, $isLocked, $has2FA, $hasDiscord, $hasPassword) { - $this->user = User::findOrFail($userId); + $this->isVerified = $isVerified; + $this->isSuspended = $isSuspended; + $this->isLocked = $isLocked; + $this->has2FA = $has2FA; + $this->hasDiscord = $hasDiscord; + $this->hasPassword = $hasPassword; } /** diff --git a/app/View/Components/ConfirmPassword.php b/app/View/Components/ConfirmPassword.php new file mode 100755 index 0000000..d44fa2a --- /dev/null +++ b/app/View/Components/ConfirmPassword.php @@ -0,0 +1,14 @@ + 'm_home', 'icon' => 'fas fa-home', - 'url' => 'dashboard', + 'url' => '/', ], [ - 'text' => 'm_directory', - 'icon' => 'fas fa-users', - 'url' => 'users/directory', - 'can' => 'profiles.view.others', + 'text' => 'Dashboard', + 'icon' => 'fas fa-tachometer-alt', + 'url' => '/dashboard' ], [ 'header' => 'h_applications', @@ -386,16 +385,16 @@ return [ [ 'text' => 'm_devtools', 'icon' => 'fas fa-code', - 'url' => '/admin/devtools', + 'route' => 'devTools', 'can' => 'admin.developertools.use', ], ], ], [ - 'text' => 'm_s_logs', - 'url' => '/admin/maintenance/system-logs', + 'text' => 'App logs', + 'url' => '/admin/developers/logs', 'icon' => 'fas fa-clipboard-list', - 'can' => 'admin.maintenance.logs.view', + 'can' => 'admin.developertools.use', ], ], diff --git a/config/app.php b/config/app.php index 9e7a632..4fa2d1d 100755 --- a/config/app.php +++ b/config/app.php @@ -94,6 +94,36 @@ return [ */ 'hide_ips' => env('HIDE_IPS'), + + /* + |-------------------------------------------------------------------------- + | Legal documents & source code + |-------------------------------------------------------------------------- + | + | Every website needs a collection of legal documents in order to remain compliant with + | international & local privacy laws. These are often best known as: + | + | - Privacy Policy, where you outline your data protection practices; + | - Terms of Service, where you outline acceptable usage of your services; + | - Community Guidelines, where you outline acceptable behavior for users on your platforms. + | + | RBRecruiter will display these URLs at appropriate locations and force users to accept them, + | if legally necessary, such as in the registration & application form pages. + | + | Additionally, you can also specify a support email and URL where your users/customers can send inquiries if necessary. + | + | You can leave these URLs empty if your website hasn't entered production yet, but we recommend + | you draft these documents as soon as possible. + | + */ + + 'terms_url' => env('TERMS_URL', '#'), + 'privacy_url' => env('PRIVACY_URL', '#'), + 'guidelines_url' => env('GUIDELINES_URL', '#'), + 'support_url' => env('SUPPORT_URL', 'https://support.example.com'), + 'support_email' => env('SUPPORT_EMAIL', 'support@example.com'), + 'source_repo' => env('SOURCE_REPO', 'https://code.webvokestudio.pt/miguel456/rbrecruiter'), + /* |-------------------------------------------------------------------------- | Application Environment @@ -261,6 +291,7 @@ return [ \App\Providers\OptionsProvider::class, App\Providers\DigitalStorageProvider::class, App\Providers\JSONProvider::class, + App\Providers\DiscordOuthProvider::class, NotificationChannels\Discord\DiscordServiceProvider::class, ], @@ -319,7 +350,8 @@ return [ 'Markdown' => GrahamCampbell\Markdown\Facades\Markdown::class, 'ContextAwareValidator' => App\Facades\ContextAwareValidation::class, 'Settings' => App\Facades\Options::class, - 'JSON' => App\Facades\JSON::class + 'JSON' => App\Facades\JSON::class, + 'DiscordOauth' => App\Facades\Discord::class ], diff --git a/config/localizator.php b/config/localizator.php old mode 100644 new mode 100755 index 1da5cfa..9dbf321 --- a/config/localizator.php +++ b/config/localizator.php @@ -40,7 +40,7 @@ return [ * Add here any custom defined functions. * NOTE: The translation string should always be the first argument. */ - 'functions' => ['__', 'trans', '@lang'] + 'functions' => ['__', 'trans_choice', 'trans', '@lang'] ], /** diff --git a/config/log-viewer.php b/config/log-viewer.php index c733cb7..08b9287 100755 --- a/config/log-viewer.php +++ b/config/log-viewer.php @@ -1,160 +1,76 @@ . - */ - -use Arcanedev\LogViewer\Contracts\Utilities\Filesystem; - return [ - /* ----------------------------------------------------------------- - | Log files storage path - | ----------------------------------------------------------------- - */ + /* + |-------------------------------------------------------------------------- + | Log Viewer Route + |-------------------------------------------------------------------------- + | Log Viewer will be available under this URL. + | + */ - 'storage-path' => storage_path('logs'), + 'route_path' => 'admin/developers/logs', - /* ----------------------------------------------------------------- - | Log files pattern - | ----------------------------------------------------------------- - */ + /* + |-------------------------------------------------------------------------- + | Back to system URL + |-------------------------------------------------------------------------- + | When set, displays a link to easily get back to this URL. + | Set to `null` to hide this link. + | + | Optional label to display for the above URL. + | + */ - 'pattern' => [ - 'prefix' => Filesystem::PATTERN_PREFIX, // 'laravel-' - 'date' => Filesystem::PATTERN_DATE, // '[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]' - 'extension' => Filesystem::PATTERN_EXTENSION, // '.log' + 'back_to_system_url' => '/admin/developers', + + 'back_to_system_label' => 'Back to Developer Tools', // Displayed by default: "Back to {{ app.name }}" + + /* + |-------------------------------------------------------------------------- + | Log Viewer route middleware. + |-------------------------------------------------------------------------- + | The middleware should enable session and cookies support in order for the Log Viewer to work. + | The 'web' middleware will be applied automatically if empty. + | + */ + + 'middleware' => ['web', 'auth'], + + /* + |-------------------------------------------------------------------------- + | Include file patterns + |-------------------------------------------------------------------------- + | + */ + + 'include_files' => ['*.log'], + + /* + |-------------------------------------------------------------------------- + | Exclude file patterns. + |-------------------------------------------------------------------------- + | This will take precedence over included files. + | + */ + + 'exclude_files' => [ + //'my_secret.log' ], - /* ----------------------------------------------------------------- - | Locale - | ----------------------------------------------------------------- - | Supported locales : - | 'auto', 'ar', 'bg', 'de', 'en', 'es', 'et', 'fa', 'fr', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', - | 'pl', 'pt-BR', 'ro', 'ru', 'sv', 'th', 'tr', 'zh-TW', 'zh' - */ + /* + |-------------------------------------------------------------------------- + | Shorter stack trace filters. + |-------------------------------------------------------------------------- + | Lines containing any of these strings will be excluded from the full log. + | This setting is only active when the function is enabled via the user interface. + | + */ - 'locale' => 'auto', - - /* ----------------------------------------------------------------- - | Theme - | ----------------------------------------------------------------- - | Supported themes : - | 'bootstrap-3', 'bootstrap-4' - | Make your own theme by adding a folder to the views directory and specifying it here. - */ - - 'theme' => 'bootstrap-4', - - /* ----------------------------------------------------------------- - | Route settings - | ----------------------------------------------------------------- - */ - - 'route' => [ - 'enabled' => true, - - 'attributes' => [ - 'prefix' => 'admin/maintenance/system-logs', - - 'middleware' => env('ARCANEDEV_LOGVIEWER_MIDDLEWARE') ? explode(',', env('ARCANEDEV_LOGVIEWER_MIDDLEWARE')) : null, - ], + 'shorter_stack_trace_excludes' => [ + '/vendor/symfony/', + '/vendor/laravel/framework/', + '/vendor/barryvdh/laravel-debugbar/', ], - - /* ----------------------------------------------------------------- - | Log entries per page - | ----------------------------------------------------------------- - | This defines how many logs & entries are displayed per page. - */ - - 'per-page' => 30, - - /* ----------------------------------------------------------------- - | Download settings - | ----------------------------------------------------------------- - */ - - 'download' => [ - 'prefix' => 'rpnetlog-', - - 'extension' => 'log', - ], - - /* ----------------------------------------------------------------- - | Menu settings - | ----------------------------------------------------------------- - */ - - 'menu' => [ - 'filter-route' => 'log-viewer::logs.filter', - - 'icons-enabled' => true, - ], - - /* ----------------------------------------------------------------- - | Icons - | ----------------------------------------------------------------- - */ - - 'icons' => [ - /** - * Font awesome >= 4.3 - * http://fontawesome.io/icons/. - */ - 'all' => 'fa fa-fw fa-list', // http://fontawesome.io/icon/list/ - 'emergency' => 'fa fa-fw fa-bug', // http://fontawesome.io/icon/bug/ - 'alert' => 'fa fa-fw fa-bullhorn', // http://fontawesome.io/icon/bullhorn/ - 'critical' => 'fa fa-fw fa-heartbeat', // http://fontawesome.io/icon/heartbeat/ - 'error' => 'fa fa-fw fa-times-circle', // http://fontawesome.io/icon/times-circle/ - 'warning' => 'fa fa-fw fa-exclamation-triangle', // http://fontawesome.io/icon/exclamation-triangle/ - 'notice' => 'fa fa-fw fa-exclamation-circle', // http://fontawesome.io/icon/exclamation-circle/ - 'info' => 'fa fa-fw fa-info-circle', // http://fontawesome.io/icon/info-circle/ - 'debug' => 'fa fa-fw fa-life-ring', // http://fontawesome.io/icon/life-ring/ - ], - - /* ----------------------------------------------------------------- - | Colors - | ----------------------------------------------------------------- - */ - - 'colors' => [ - 'levels' => [ - 'empty' => '#D1D1D1', - 'all' => '#8A8A8A', - 'emergency' => '#B71C1C', - 'alert' => '#D32F2F', - 'critical' => '#F44336', - 'error' => '#FF5722', - 'warning' => '#FF9100', - 'notice' => '#4CAF50', - 'info' => '#1976D2', - 'debug' => '#90CAF9', - ], - ], - - /* ----------------------------------------------------------------- - | Strings to highlight in stack trace - | ----------------------------------------------------------------- - */ - - 'highlight' => [ - '^#\d+', - '^Stack trace:', - ], - ]; diff --git a/config/services.php b/config/services.php index 1f6ab63..87a0eaf 100755 --- a/config/services.php +++ b/config/services.php @@ -37,12 +37,18 @@ return [ 'client_id' => env('DISCORD_CLIENT_ID'), 'client_secret' => env('DISCORD_CLIENT_SECRET'), 'redirect' => env('DISCORD_REDIRECT_URI'), + 'base_url' => env('DISCORD_BASE_URL'), 'token' => env('DISCORD_BOT_TOKEN'), + 'staff_guild_roles' => env('DISCORD_STAFF_GUILD_ROLES'), + 'staff_home_roles' => env('DISCORD_HOME_GUILD_ROLES'), + // optional 'allow_gif_avatars' => (bool)env('DISCORD_AVATAR_GIF', true), 'avatar_default_extension' => env('DISCORD_EXTENSION_DEFAULT', 'jpg'), // only pick from jpg, png, webp + 'home_guild' => env('DISCORD_HOME_GUILD'), + 'staff_guild' => env('DISCORD_STAFF_GUILD', null) ], 'mailgun' => [ @@ -66,4 +72,7 @@ return [ 'beams_secret_key' => 'Your Secret Key', ], + 'cpanel' => [ + 'api_token' => env('CPANEL_API_TOKEN', null) + ] ]; diff --git a/database/factories/ProfileFactory.php b/database/factories/ProfileFactory.php new file mode 100755 index 0000000..dfe3c1b --- /dev/null +++ b/database/factories/ProfileFactory.php @@ -0,0 +1,23 @@ + + */ +class ProfileFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + // + ]; + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 1bbe73f..c7c9b65 100755 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -1,47 +1,36 @@ . - */ - namespace Database\Factories; -use App\User; -use Faker\Generator as Faker; -use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Factories\Factory; +use Illuminate\Support\Carbon; -/* -|-------------------------------------------------------------------------- -| Model Factories -|-------------------------------------------------------------------------- -| -| This directory should contain each of the model factory definitions for -| your application. Factories provide a convenient way to generate new -| model instances for testing / seeding your application's database. -| -*/ - -$factory->define(User::class, function (Faker $faker) { - return [ - 'name' => $faker->name, - 'email' => $faker->unique()->safeEmail, - 'email_verified_at' => now(), - 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password - 'remember_token' => Str::random(10), - ]; -}); +/** + * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\User> + */ +class UserFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'uuid' => $this->faker->uuid(), + 'name' => $this->faker->name(), + 'email' => $this->faker->unique()->safeEmail(), + 'email_verified_at' => Carbon::now(), + 'username' => $this->faker->userName(), + 'dob' => Carbon::now(), + 'password' => bcrypt($this->faker->password), + 'created_at' => Carbon::now(), + 'updated_at' => Carbon::now(), + 'password_last_updated' => Carbon::now(), + 'administratively_locked' => false, + 'registrationIp' => $this->faker->ipv4(), + 'currentIp' => $this->faker->ipv4(), + ]; + } +} diff --git a/database/migrations/.gitkeep b/database/migrations/.gitkeep old mode 100644 new mode 100755 diff --git a/database/migrations/2022_03_07_180241_remove_account_tokens_from_user.php b/database/migrations/2022_03_07_180241_remove_account_tokens_from_user.php old mode 100644 new mode 100755 diff --git a/database/migrations/2022_08_14_210735_add_registration_i_p_to_user.php b/database/migrations/2022_08_14_210735_add_registration_i_p_to_user.php new file mode 100755 index 0000000..16771fd --- /dev/null +++ b/database/migrations/2022_08_14_210735_add_registration_i_p_to_user.php @@ -0,0 +1,34 @@ +renameColumn('originalIP', 'currentIp'); + $table->ipAddress('registrationIp')->after('originalIP'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->renameColumn('currentIp', 'originalIP'); + $table->removeColumn('ipAddress'); + }); + } +}; diff --git a/database/migrations/2022_08_20_195538_add_discord_to_users.php b/database/migrations/2022_08_20_195538_add_discord_to_users.php new file mode 100755 index 0000000..a60df7f --- /dev/null +++ b/database/migrations/2022_08_20_195538_add_discord_to_users.php @@ -0,0 +1,41 @@ +string('password')->nullable()->change(); // social login requires no pw (still required upon normal reg) + $table->string('registrationIp')->nullable()->change(); + + $table->unsignedBigInteger('discord_user_id')->after('remember_token')->nullable(); + $table->longText('discord_token')->after('discord_user_id')->nullable(); + $table->longText('discord_refresh_token')->after('discord_token')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('discord_token', 'discord_refresh_token', 'discord_user_id'); + + $table->string('password')->nullable(false)->change(); + $table->string('registrationIp')->nullable(false)->change(); + }); + } +}; diff --git a/database/migrations/2022_08_27_052451_add_int_profile_pic_to_users.php b/database/migrations/2022_08_27_052451_add_int_profile_pic_to_users.php new file mode 100755 index 0000000..68d46bd --- /dev/null +++ b/database/migrations/2022_08_27_052451_add_int_profile_pic_to_users.php @@ -0,0 +1,22 @@ +longText('discord_pfp')->after('discord_user_id')->nullable(); + }); + } + + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('discord_pfp'); + }); + } +} diff --git a/database/migrations/2022_09_04_133123_add_locale_to_users.php b/database/migrations/2022_09_04_133123_add_locale_to_users.php new file mode 100755 index 0000000..c98cad7 --- /dev/null +++ b/database/migrations/2022_09_04_133123_add_locale_to_users.php @@ -0,0 +1,22 @@ +string('locale')->after('email')->default('en-US'); + }); + } + + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('locale'); + }); + } +} diff --git a/database/migrations/2022_10_15_023108_update_vacancy_integration_nullable.php b/database/migrations/2022_10_15_023108_update_vacancy_integration_nullable.php new file mode 100644 index 0000000..eb79dd3 --- /dev/null +++ b/database/migrations/2022_10_15_023108_update_vacancy_integration_nullable.php @@ -0,0 +1,24 @@ +string('permissionGroupName')->nullable()->change(); + $table->string('discordRoleID')->nullable()->change(); + }); + } + + public function down() + { + Schema::table('vacancies', function (Blueprint $table) { + $table->string('permissionGroupName')->nullable(false)->change(); + $table->string('discordRoleID')->nullable(false)->change(); + }); + } +} diff --git a/database/migrations/2022_10_22_014237_add_discord_requirement_to_vacancies.php b/database/migrations/2022_10_22_014237_add_discord_requirement_to_vacancies.php new file mode 100644 index 0000000..396939d --- /dev/null +++ b/database/migrations/2022_10_22_014237_add_discord_requirement_to_vacancies.php @@ -0,0 +1,24 @@ +boolean('requiresDiscord') + ->default(false) + ->after('vacancyStatus'); + }); + } + + public function down() + { + Schema::table('vacancies', function (Blueprint $table) { + $table->dropColumn('requiresDiscord'); + }); + } +} diff --git a/database/migrations/2022_10_23_174610_add_required_age_to_vacancies.php b/database/migrations/2022_10_23_174610_add_required_age_to_vacancies.php new file mode 100644 index 0000000..a17efb1 --- /dev/null +++ b/database/migrations/2022_10_23_174610_add_required_age_to_vacancies.php @@ -0,0 +1,22 @@ +integer('requiredAge')->default(16)->after('vacancyStatus'); + }); + } + + public function down() + { + Schema::table('vacancies', function (Blueprint $table) { + $table->dropColumn('requiredAge'); + }); + } +} diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php index 03c6118..4decf3a 100755 --- a/database/seeders/PermissionSeeder.php +++ b/database/seeders/PermissionSeeder.php @@ -42,6 +42,10 @@ class PermissionSeeder extends Seeder ] ); + $staff = Role::create([ + 'name' => 'staff' + ]); + $reviewer = Role::create( [ 'name' => 'reviewer' @@ -108,15 +112,18 @@ class PermissionSeeder extends Seeder 'profiles.view.others' ]); - // Able to view applications and vote on them once they reach the right stage, but not approve applications up to said stage - $reviewer->givePermissionTo([ - 'applications.view.all', - 'applications.vote', + $staff->givePermissionTo([ 'reviewer.viewAbsence', 'reviewer.requestAbsence', 'reviewer.withdrawAbsence', ]); + // Able to view applications and vote on them once they reach the right stage, but not approve applications up to said stage + $reviewer->givePermissionTo([ + 'applications.view.all', + 'applications.vote' + ]); + $hiringManager->givePermissionTo('appointments.*', 'applications.*', 'admin.hiring.*'); $admin->givePermissionTo([ diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php index d76b7ff..6e833dd 100755 --- a/database/seeders/UserSeeder.php +++ b/database/seeders/UserSeeder.php @@ -34,144 +34,36 @@ class UserSeeder extends Seeder */ public function run() { - + $ghostAccount = User::create([ 'uuid' => 'b741345057274a519144881927be0290', // Ghost 'name' => 'Ghost (deleted account)', 'email' => 'blackhole@example.com', 'email_verified_at' => now(), 'username' => 'ghost', - 'originalIP' => '0.0.0.0', + 'registrationIp' => '0.0.0.0', + 'currentIp' => '0.0.0.0', 'password' => 'locked' ])->assignRole('user'); // There can't be role-less users - $staffUsers = [ - - [ - 'uuid' => 'd2b321b56ff1445db9d7794701983cad', - 'name' => 'Robot 1', - 'email' => 'tester1@example.com', - 'username' => 'tester1', - 'originalIP' => '99.18.146.235', - 'password' => Hash::make('password') - ], - [ - 'uuid' => 'ab22b5da02644953ace969fce85c0819', - 'name' => 'Robot 2', - 'email' => 'tester2@example.com', - 'username' => 'tester2', - 'originalIP' => '141.239.229.53', - 'password' => Hash::make('password') - ], - [ - 'uuid' => 'df38e6bf762944d3a600ded59a693ad1', - 'name' => 'Robot 3', - 'email' => 'tester3@example.com', - 'username' => 'tester3', - 'originalIP' => '25.63.20.97', - 'password' => Hash::make('password') - ], - [ - 'uuid' => '689e446484824f6bad5064e3df0aaa96', - 'name' => 'Robot 4', - 'email' => 'tester4@example.com', - 'username' => 'tester4', - 'originalIP' => '220.105.223.142', - 'password' => Hash::make('password') - ], - [ - 'uuid' => '172391f917bf418ab1c40ebc041ed5ba', - 'name' => 'Robot 5', - 'email' => 'tester5@example.com', - 'username' => 'tester5', - 'originalIP' => '224.66.76.60', - 'password' => Hash::make('password') - ], - [ - 'uuid' => '371f34dcce2a4457bf385ab9417a2345', - 'name' => 'Robot 6', - 'email' => 'tester6@example.com', - 'username' => 'tester6', - 'originalIP' => '97.113.131.0', - 'password' => Hash::make('password') - ], - [ - 'uuid' => '89aa5222855542bebe7a7780248ef5f9', - 'name' => 'Robot 7', - 'email' => 'tester7@example.com', - 'username' => 'tester7', - 'originalIP' => '15.160.137.222', - 'password' => Hash::make('password') - ], - - ]; - - $regularUsers = [ - - [ - 'uuid' => '20f69f47e72f463493b5b91d1c05452f', - 'name' => 'User 1', - 'email' => 'user1@example.com', - 'username' => 'user1', - 'originalIP' => '253.25.237.78', - 'password' => Hash::make('password') - ], - [ - 'uuid' => '5f900018241e4aaba7883f2d5c5c2357', - 'name' => 'User 2', - 'email' => 'user2@example.com', - 'username' => 'user2', - 'originalIP' => '82.92.156.176', - 'password' => Hash::make('password') - ], - [ - 'uuid' => 'ba9780c3270745c6840eaabe1bf8aa14', - 'name' => 'User 3', - 'email' => 'user3@example.com', - 'username' => 'user3', - 'originalIP' => '224.123.129.17', - 'password' => Hash::make('password') - ] - - ]; - - foreach ($regularUsers as $regularUser) - { - $user = User::create($regularUser); - Profile::create([ - 'profileShortBio' => 'Random data ' . rand(0,1000), - 'profileAboutMe' => 'Random data ' . rand(0, 1000), - 'socialLinks' => "[]", // empty json set, not an array - 'avatarPreference' => 'gravatar', - 'userID' => $user->id - ]); - } - - foreach($staffUsers as $staffUser) - { - $user = User::create($staffUser); - Profile::create([ - 'profileShortBio' => 'Random data ' . rand(0,1000), - 'profileAboutMe' => 'Random data ' . rand(0, 1000), - 'socialLinks' => "[]", - 'avatarPreference' => 'gravatar', - 'userID' => $user->id - ]); - - } - - User::create([ - 'uuid' => '6102256abd284dd7b68e4c96ef313734', + $admin = User::create(([ + 'uuid' => 'b741345057274a519144881927be0290', // Ghost 'name' => 'Admin', - 'email' => 'admin@example.com', + 'email' => 'admin@webvokestudio.pt', + 'email_verified_at' => now(), 'username' => 'admin', - 'originalIP' => '192.168.1.2', - 'password' => Hash::make('password') - ]); + 'registrationIp' => '0.0.0.0', + 'currentIp' => '0.0.0.0', + 'password' => Hash::make('admin') + ]))->assignRole('user', 'reviewer', 'hiringManager', 'admin'); - foreach (User::all() as $user) - { - $user->assignRole('reviewer', 'user'); + $users = User::factory() + ->count(1500) + ->create(); + + foreach ($users as $user) { + $user->assignRole('user'); } + } } diff --git a/dependabot.yml b/dependabot.yml old mode 100644 new mode 100755 diff --git a/lang/en.json b/lang/en.json old mode 100644 new mode 100755 index 7d44e72..0287b67 --- a/lang/en.json +++ b/lang/en.json @@ -39,6 +39,7 @@ "Account": "Account", "Account already suspended!": "Account already suspended!", "Account deletion": "Account deletion", + "Account Management": "Account Management", "Account management (admin)": "Account management (admin)", "Account Security": "Account Security", "Account Settings": "Account Settings", @@ -57,6 +58,7 @@ "Administration": "Administration", "Administrative actions such as:": "Administrative actions such as:", "Admin logs": "Admin logs", + "After you delete your profile, the following will happen:": "After you delete your profile, the following will happen:", "A leave of absence allows you to step away from your duties for a period of time. To request one, simply fill the form to your left, and enter the reason for which you're stepping away. You will also need to specify when you will be unavailable, and when you predict to be back.": "A leave of absence allows you to step away from your duties for a period of time. To request one, simply fill the form to your left, and enter the reason for which you're stepping away. You will also need to specify when you will be unavailable, and when you predict to be back.", "A leave of absence is a time period in which an employee takes personal time off, for a multitude of reasons. It's a prolonged, authorized absence form work and\/or other duties, communicated in advance, usually via letter or via an HR system.": "A leave of absence is a time period in which an employee takes personal time off, for a multitude of reasons. It's a prolonged, authorized absence form work and\/or other duties, communicated in advance, usually via letter or via an HR system.", "All Applications": "All Applications", @@ -75,6 +77,7 @@ "Applicant IP Address": "Applicant IP Address", "Applicant Name": "Applicant Name", "Application": "Application", + "Application-specific commands": "Application-specific commands", "Application access denied": "Application access denied", "Application Date": "Application Date", "Application deleted. Comments, appointments and responses have also been deleted.": "Application deleted. Comments, appointments and responses have also been deleted.", @@ -82,6 +85,8 @@ "Application form management tool": "Application form management tool", "Application ID": "Application ID", "Application Management": "Application Management", + "Application Override: Approve": "Application Override: Approve", + "Application Override: Decline": "Application Override: Decline", "Application Process": "Application Process", "Applications": "Applications", "Applications Closed": "Applications Closed", @@ -102,6 +107,7 @@ "Are you sure you want to cancel this appointment? The user will be notified of this via email, and you will be able to reschedule.": "Are you sure you want to cancel this appointment? The user will be notified of this via email, and you will be able to reschedule.", "Are you sure you want to delete this log file: :date ?": "Are you sure you want to delete this log file: :date ?", "Are you sure you want to DELETE this log file: :date ?": "Are you sure you want to DELETE this log file: :date ?", + "Are you sure you want to delete your profile?": "Are you sure you want to delete your profile?", "Are you sure you want to deny this application? Please keep in mind that this user will only be allowed to apply 30 days after their first application.": "Are you sure you want to deny this application? Please keep in mind that this user will only be allowed to apply 30 days after their first application.", "Are you sure you want to leave the form builder? You have unsaved work.": "Are you sure you want to leave the form builder? You have unsaved work.", "Are you sure you want to submit your application? Please review each of your answers carefully before doing so.": "Are you sure you want to submit your application? Please review each of your answers carefully before doing so.", @@ -140,6 +146,7 @@ "Choose a field type": "Choose a field type", "Choose a game in the section below, if applicable.": "Choose a game in the section below, if applicable.", "Choose an application": "Choose an application", + "Choose an application to override": "Choose an application to override", "Choose a security policy": "Choose a security policy", "Choose a user to invite": "Choose a user to invite", "Choose File (max. :maxFileSizeSettingValue)": "Choose File (max. :maxFileSizeSettingValue)", @@ -169,11 +176,14 @@ "Context": "Context", "Contextual information": "Contextual information", "Continue": "Continue", + "Copyright © :currentYear :authorName — :licenseFullName<\/a>": "Copyright © :currentYear :authorName — :licenseFullName<\/a>", "Count all votes now": "Count all votes now", "Counts and processes all backlogged votes, for all applications.": "Counts and processes all backlogged votes, for all applications.", "Create": "Create", "Create a form first, then, create a vacancy.": "Create a form first, then, create a vacancy.", + "Create a profile": "Create a profile", "Created at": "Created at", + "Create profile": "Create profile", "Current Email Address": "Current Email Address", "Current from (uneditable)": "Current from (uneditable)", "Current Password": "Current Password", @@ -182,14 +192,17 @@ "Dashboard": "Dashboard", "Date": "Date", "Decision & Moderation Tools": "Decision & Moderation Tools", + "Decline application": "Decline application", "Declined": "Declined", "Declined by an admin or withdrawn by the requester": "Declined by an admin or withdrawn by the requester", "Delete": "Delete", "Delete Account": "Delete Account", "Delete log file": "Delete log file", + "Delete Profile": "Delete Profile", "Delete request": "Delete request", "Deleting a user's account is an irreversible process. Historic and current applications, votes, and profile content, as well as any personally identifiable information will be immediately erased.": "Deleting a user's account is an irreversible process. Historic and current applications, votes, and profile content, as well as any personally identifiable information will be immediately erased.", "Deleting your account is an irreversible process. The following data will be deleted (including personally identifiable data):": "Deleting your account is an irreversible process. The following data will be deleted (including personally identifiable data):", + "Deleting your profile is an irreversible operation. You will not be able to recover any previously entered information.": "Deleting your profile is an irreversible operation. You will not be able to recover any previously entered information.", "Demo accounts:": "Demo accounts:", "Demo mode disables some app features in order to preserve it's integrity for everyone who wants to test it. Here's what's disabled: ": "Demo mode disables some app features in order to preserve it's integrity for everyone who wants to test it. Here's what's disabled: ", "Demo mode is active on this instance. The database is refreshed daily and some features are disabled for security reasons.": "Demo mode is active on this instance. The database is refreshed daily and some features are disabled for security reasons.", @@ -208,6 +221,7 @@ "Discord Handle": "Discord Handle", "Discord Role ID": "Discord Role ID", "Discord role ID": "Discord role ID", + "Discord tag: :discordTag": "Discord tag: :discordTag", "Dispatch approval event": "Dispatch approval event", "Dispatches an approval event for the selected application": "Dispatches an approval event for the selected application", "Dispatches a rejection event for the selected application": "Dispatches a rejection event for the selected application", @@ -222,8 +236,10 @@ "Duration": "Duration", "e.g. Spamming": "e.g. Spamming", "Edit": "Edit", + "Edit account": "Edit account", "Edit Account": "Edit Account", "Edit form": "Edit form", + "Editing :formTitle...": "Editing :formTitle...", "Editor": "Editor", "Edit Team": "Edit Team", "Edit vacancies": "Edit vacancies", @@ -236,6 +252,7 @@ "Ended": "Ended", "entries": "entries", "ENV": "ENV", + "Error": "Error", "European?": "European?", "Event dispatched; Candidate approval sequence initiated.": "Event dispatched; Candidate approval sequence initiated.", "Event dispatched; Candidate rejection sequence initiated.": "Event dispatched; Candidate rejection sequence initiated.", @@ -288,14 +305,20 @@ "High (╯°□°)╯︵ ┻━┻": "High (╯°□°)╯︵ ┻━┻", "Home": "Home", "Hooray! 2FA is setup correctly for your account. A code will be asked each time you login.": "Hooray! 2FA is setup correctly for your account. A code will be asked each time you login.", + "Housekeeping": "Housekeeping", + "Housekeeping jobs usually run once every day, but if one of them has failed for some reason, you can manually run them here.": "Housekeeping jobs usually run once every day, but if one of them has failed for some reason, you can manually run them here.", "How to request a leave of absence": "How to request a leave of absence", "Human Resources": "Human Resources", + "I am 13 years of age or older and have read and agree with the Community Guidelines<\/a>, Privacy Policy<\/a> and Terms of Service<\/a> set forth.": "I am 13 years of age or older and have read and agree with the Community Guidelines<\/a>, Privacy Policy<\/a> and Terms of Service<\/a> set forth.", "If an interview is scheduled, you'll need to open your application here and confirm the time, date, and location assigned for you.": "If an interview is scheduled, you'll need to open your application here and confirm the time, date, and location assigned for you.", + "If the setting \"Require Valid Game License\" is activated, editing this field may have unintended consequences. Proceed with caution.": "If the setting \"Require Valid Game License\" is activated, editing this field may have unintended consequences. Proceed with caution.", "If this form has been updated, new fields and updated questions will not show up here!": "If this form has been updated, new fields and updated questions will not show up here!", "If you'd like to learn more about our community, make sure to visit our main website<\/a>!": "If you'd like to learn more about our community, make sure to visit our main website<\/a>!", "If you're seeing this message in error, please contact your system administrator.": "If you're seeing this message in error, please contact your system administrator.", + "If you change your mind and want your profile back, you will be able to create a new blank profile from your profile configuration page, restoring access to the features mentioned above.": "If you change your mind and want your profile back, you will be able to create a new blank profile from your profile configuration page, restoring access to the features mentioned above.", "If you weren't present during this meeting, you can view the shared meeting notepad to help you make a decision.": "If you weren't present during this meeting, you can view the shared meeting notepad to help you make a decision.", "Image Describing Access Denied": "Image Describing Access Denied", + "In a future update, the public Directory page might become inaccessible to users without a profile.": "In a future update, the public Directory page might become inaccessible to users without a profile.", "Incorrect code. Please reopen the 2FA settings panel and try again.": "Incorrect code. Please reopen the 2FA settings panel and try again.", "in days": "in days", "Ineligible (:days) day(s) remaining": "Ineligible (:days) day(s) remaining", @@ -359,6 +382,7 @@ "Mojang Logo (Minecraft)": "Mojang Logo (Minecraft)", "Mojang UUID (deprecated)": "Mojang UUID (deprecated)", "Move to next stage": "Move to next stage", + "Multi-factor authentication is enabled for your account.": "Multi-factor authentication is enabled for your account.", "Must contain numerical characters": "Must contain numerical characters", "Must contain special characters": "Must contain special characters", "Must contain upper and lower case characters": "Must contain upper and lower case characters", @@ -383,6 +407,7 @@ "No": "No", "No details yet... Add some!": "No details yet... Add some!", "None yet": "None yet", + "No profile found!": "No profile found!", "No requests": "No requests", "Not available": "Not available", "Note! The database is wiped every six hours during demo mode.": "Note! The database is wiped every six hours during demo mode.", @@ -406,6 +431,7 @@ "Our services are currently undergoing routine maintenance. We are sorry for any inconveniences caused! We'll be back ASAP.": "Our services are currently undergoing routine maintenance. We are sorry for any inconveniences caused! We'll be back ASAP.", "Outstanding": "Outstanding", "Outstanding (Submitted)": "Outstanding (Submitted)", + "Override now": "Override now", "Page :current of :last": "Page :current of :last", "Password": "Password", "Password change": "Password change", @@ -419,6 +445,7 @@ "permanently.": "permanently.", "Permission group": "Permission group", "Platform": "Platform", + "Please accept the Community Guidelines, Terms of Service and Privacy Policy to continue.": "Please accept the Community Guidelines, Terms of Service and Privacy Policy to continue.", "Please allow up to three days for your application to be processed. Your application will be reviewed by every team member, and will move up in stages.": "Please allow up to three days for your application to be processed. Your application will be reviewed by every team member, and will move up in stages.", "Please authenticate": "Please authenticate", "Please change update your password now. You won't be able to use the site until you do this.": "Please change update your password now. You won't be able to use the site until you do this.", @@ -430,6 +457,7 @@ "Please confirm that you want to suspend this account. You'll need to add a reason and expiration date to confirm this.": "Please confirm that you want to suspend this account. You'll need to add a reason and expiration date to confirm this.", "Please contact your administrator if you believe this was in error.": "Please contact your administrator if you believe this was in error.", "Please do not tamper with the URLs. To report a bug, please contact an administrator.": "Please do not tamper with the URLs. To report a bug, please contact an administrator.", + "Please enter a valid (and Premium) Minecraft username! We do not support cracked users.": "Please enter a valid (and Premium) Minecraft username! We do not support cracked users.", "Please fill out the form below. Keep all answers concise and complete. Please keep in mind that the age requirement is at least :ageUpperLimitSettingValue years old.": "Please fill out the form below. Keep all answers concise and complete. Please keep in mind that the age requirement is at least :ageUpperLimitSettingValue years old.", "Please note: Applications CANNOT be modified once they're submitted!": "Please note: Applications CANNOT be modified once they're submitted!", "Please type the above text": "Please type the above text", @@ -446,6 +474,7 @@ "Private": "Private", "Privilege editing": "Privilege editing", "Profile": "Profile", + "Profile deleted successfully.": "Profile deleted successfully.", "Profile updated.": "Profile updated.", "Public note": "Public note", "Public Team": "Public Team", @@ -479,6 +508,9 @@ "Reviewer": "Reviewer", "Reviewing admin": "Reviewing admin", "Roles": "Roles", + "Run task: end expired absence requests": "Run task: end expired absence requests", + "Run task: lift expired suspensions": "Run task: lift expired suspensions", + "Run task: process pending votes": "Run task: process pending votes", "Same as Medium, but: ": "Same as Medium, but: ", "Save & Quit": "Save & Quit", "Save and Close": "Save and Close", @@ -510,6 +542,7 @@ "Sign-in": "Sign-in", "Sign in": "Sign in", "Sign in here": "Sign in here", + "Sign in to your account": "Sign in to your account", "Sign out": "Sign out", "Sign up": "Sign up", "Sign up for an account": "Sign up for an account", @@ -534,6 +567,7 @@ "Submit for approval": "Submit for approval", "Submitted": "Submitted", "Submitted at": "Submitted at", + "Success": "Success", "Successfully logged out other devices. Remember to change your password if you think you've been compromised.": "Successfully logged out other devices. Remember to change your password if you think you've been compromised.", "Supported apps you can install:": "Supported apps you can install:", "Suspend": "Suspend", @@ -576,6 +610,7 @@ "The password is \":demoPassword\" for all accounts.": "The password is \":demoPassword\" for all accounts.", "The permission group from your server\/network's permissions manager. Compatible with Luckperms and PEX (This feature is deprecated and will be removed on a future version).": "The permission group from your server\/network's permissions manager. Compatible with Luckperms and PEX (This feature is deprecated and will be removed on a future version).", "The position you're trying to update doesn't exist!": "The position you're trying to update doesn't exist!", + "There are many benefits to creating a profile! For instance, you'll be able to set a profile picture, as well as sharing your social media and anything else you'd like. Creating a profile is instant and you can begin configuring it right away. If you later change your mind, you may also delete your profile at any time.": "There are many benefits to creating a profile! For instance, you'll be able to set a profile picture, as well as sharing your social media and anything else you'd like. Creating a profile is instant and you can begin configuring it right away. If you later change your mind, you may also delete your profile at any time.", "There are no applications here": "There are no applications here", "There are no comments here! Comments are only visible to staff members. Be the first to share your input! Commenting may help with decision-making when time comes to vote for an application.": "There are no comments here! Comments are only visible to staff members. Be the first to share your input! Commenting may help with decision-making when time comes to vote for an application.", "There are no notes yet. Add some!": "There are no notes yet. Add some!", @@ -588,6 +623,7 @@ "There was an error deleting the file: :msg": "There was an error deleting the file: :msg", "There were :usersCount user(s) matching your search.": "There were :usersCount user(s) matching your search.", "There were no expired suspensions (or no suspensions at all) to purge.": "There were no expired suspensions (or no suspensions at all) to purge.", + "These tools were intended for development purposes. Unless you know exactly what each command does, we recommend you don't use any of them.": "These tools were intended for development purposes. Unless you know exactly what each command does, we recommend you don't use any of them.", "The vacancies you select determine what applications your team members see. All applications under the vacancies you choose will be displayed.": "The vacancies you select determine what applications your team members see. All applications under the vacancies you choose will be displayed.", "This account has been suspended :suspensionTypeValue": "This account has been suspended :suspensionTypeValue", "This account isn't suspended!": "This account isn't suspended!", @@ -599,10 +635,12 @@ "This is how your form looks like to applicants.": "This is how your form looks like to applicants.", "This is the name team members will see.": "This is the name team members will see.", "This opening does not have any details yet.": "This opening does not have any details yet.", + "This panel allows you to override statuses for specific applications. Overriding them will trigger the correct events as well. Note that this system entirely ignores the voting system because these statuses ignore all other logic.": "This panel allows you to override statuses for specific applications. Overriding them will trigger the correct events as well. Note that this system entirely ignores the voting system because these statuses ignore all other logic.", "THIS PROCESS IS IRREVERSIBLE AND IMMEDIATE": "THIS PROCESS IS IRREVERSIBLE AND IMMEDIATE", "This query didn't return any results.": "This query didn't return any results.", "This request reached its predicted end date": "This request reached its predicted end date", "This setting controls whether people can join the team freely.": "This setting controls whether people can join the team freely.", + "This user has been suspended.": "This user has been suspended.", "This user has been suspended by the admins. Admins suspend accounts for a variety of reasons, including spam.": "This user has been suspended by the admins. Admins suspend accounts for a variety of reasons, including spam.", "This user will receive an email notification asking them to join your team.": "This user will receive an email notification asking them to join your team.", "This vacancy does not have any details yet.": "This vacancy does not have any details yet.", @@ -638,6 +676,7 @@ "User invited successfully!": "User invited successfully!", "User profile picture": "User profile picture", "Users": "Users", + "Users \/ Accounts \/ Admin": "Users \/ Accounts \/ Admin", "Users will be locked out after this time period if they fail to enable 2FA. Leave empty to disable.": "Users will be locked out after this time period if they fail to enable 2FA. Leave empty to disable.", "User terminated successfully.": "User terminated successfully.", "User updated successfully!": "User updated successfully!", @@ -682,6 +721,7 @@ "Will you be available to assist occasionally during your absence?": "Will you be available to assist occasionally during your absence?", "Wrong confirmation text! Try again.": "Wrong confirmation text! Try again.", "Yes": "Yes", + "Yes, delete my profile": "Yes, delete my profile", "You're looking at all applications ever received": "You're looking at all applications ever received", "You're not authorized to access this page.": "You're not authorized to access this page.", "You've been redirected here because your password has expired. All users must change their password every :numDaysChangePw days. This is put in place to make sure user accounts remain secure.": "You've been redirected here because your password has expired. All users must change their password every :numDaysChangePw days. This is put in place to make sure user accounts remain secure.", @@ -703,8 +743,10 @@ "You may only have one active request at the same time, which will have to be either approved or declined by the admins. Please keep in mind that you will not be able to delete any of your requests.": "You may only have one active request at the same time, which will have to be either approved or declined by the admins. Please keep in mind that you will not be able to delete any of your requests.", "You may vote on as many applications as needed; However, you can only vote once per application.": "You may vote on as many applications as needed; However, you can only vote once per application.", "You need to be authenticated to access this page. Believe this is a mistake? Contact us and let us know! ": "You need to be authenticated to access this page. Believe this is a mistake? Contact us and let us know! ", + "Your account currently has no profile on file. This means that your account will not be visibile in the public profile Directory, and you will not be able to access your profile until you create one.": "Your account currently has no profile on file. This means that your account will not be visibile in the public profile Directory, and you will not be able to access your profile until you create one.", "Your account is not permitted to submit another application. Please wait :applicationThrottleLimitSettingValue more days before trying to submit an application.": "Your account is not permitted to submit another application. Please wait :applicationThrottleLimitSettingValue more days before trying to submit an application.", "Your account will be locked during this process.": "Your account will be locked during this process.", + "Your account will no longer be listed on the public Directory page.": "Your account will no longer be listed on the public Directory page.", "Your comments": "Your comments", "Your current password security policy is set to off<\/b>. This allows users to choose potentially unsafe passwords. We strongly recommend you update this value to Medium<\/b>.": "Your current password security policy is set to off<\/b>. This allows users to choose potentially unsafe passwords. We strongly recommend you update this value to Medium<\/b>.", "Your current session: logged in from :ipAddress": "Your current session: logged in from :ipAddress", @@ -713,12 +755,16 @@ "Your message": "Your message", "Your password has expired": "Your password has expired", "Your previous applications": "Your previous applications", + "Your profile configuration page will become inaccessible with an error message.": "Your profile configuration page will become inaccessible with an error message.", "Your profile data and preferences": "Your profile data and preferences", + "Your profile has been created.": "Your profile has been created.", + "Your profile page will become inaccesible to everyone with an error message.": "Your profile page will become inaccesible to everyone with an error message.", "Your request": "Your request", "Your roles": "Your roles", "Your search term did not return any results.": "Your search term did not return any results.", "Your upcoming interviews": "Your upcoming interviews", "Your vote has been counted!": "Your vote has been counted!", "You will also need to agree to the terms of a LOA. Additionally, you may also specify whether you'll be available to chat occasionally during your absence, but not perform your full duties.": "You will also need to agree to the terms of a LOA. Additionally, you may also specify whether you'll be available to chat occasionally during your absence, but not perform your full duties.", - "Zoom": "Zoom" + "Zoom": "Zoom", + "{1} There is :count open position!|[2,*] There are :count open positions!": "{1} There is :count open position!|[2,*] There are :count open positions!" } \ No newline at end of file diff --git a/lang/en/adminlte::adminlte.php b/lang/en/adminlte::adminlte.php old mode 100644 new mode 100755 diff --git a/lang/en/log-viewer::general.php b/lang/en/log-viewer::general.php old mode 100644 new mode 100755 diff --git a/lang/en/messages.php b/lang/en/messages.php deleted file mode 100644 index 4cd398a..0000000 --- a/lang/en/messages.php +++ /dev/null @@ -1,28 +0,0 @@ - '', - 'choose_app' => '', - 'dashboard' => '', - 'details_m_title' => '', - 'dispatch_event' => '', - 'edt_action' => '', - 'global_error' => '', - 'global_success' => '', - 'modal_close' => '', - 'open' => '', - 'ppolicy' => '', - 'profile' => [ - 'discord_tag' => '', - 'edituser' => '', - 'edituser_consequence' => '', - ], - 'reusable' => [ - 'confirm' => '', - ], - 'signin_cta' => '', - 'terms' => '', - 'view_app' => [ - 'denyapp' => '', - ], -]; diff --git a/lang/en/pagination.php b/lang/en/pagination.php old mode 100644 new mode 100755 diff --git a/lang/en/passwords.php b/lang/en/passwords.php new file mode 100755 index 0000000..fb0e6b8 --- /dev/null +++ b/lang/en/passwords.php @@ -0,0 +1,9 @@ + 'Your password has been reset!', + 'sent' => 'We have emailed your password reset link!', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => 'We can\'t find a user with that email address.', +]; diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100755 index 0000000..b3eaf74 --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,115 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'attributes' => [ + ], + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute must have between :min and :max items.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute must be between :min and :max.', + 'string' => 'The :attribute must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute must have more than :value items.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'numeric' => 'The :attribute must be greater than :value.', + 'string' => 'The :attribute must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute must have :value items or more.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => 'The :attribute must be an integer.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'array' => 'The :attribute must have less than :value items.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'numeric' => 'The :attribute must be less than :value.', + 'string' => 'The :attribute must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute must not have more than :value items.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'numeric' => 'The :attribute must be less than or equal :value.', + 'string' => 'The :attribute must be less than or equal :value characters.', + ], + 'max' => [ + 'array' => 'The :attribute may not have more than :max items.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'numeric' => 'The :attribute may not be greater than :max.', + 'string' => 'The :attribute may not be greater than :max characters.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute must have at least :min items.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'numeric' => 'The :attribute must be at least :min.', + 'string' => 'The :attribute must be at least :min characters.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'array' => 'The :attribute must contain :size items.', + 'file' => 'The :attribute must be :size kilobytes.', + 'numeric' => 'The :attribute must be :size.', + 'string' => 'The :attribute must be :size characters.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', +]; diff --git a/lang/pt-br.json b/lang/pt-br.json old mode 100644 new mode 100755 index ece2638..149ddd7 --- a/lang/pt-br.json +++ b/lang/pt-br.json @@ -39,6 +39,7 @@ "Account": "", "Account already suspended!": "", "Account deletion": "", + "Account Management": "", "Account management (admin)": "", "Account Security": "", "Account Settings": "", @@ -57,6 +58,7 @@ "Administration": "", "Administrative actions such as:": "", "Admin logs": "", + "After you delete your profile, the following will happen:": "", "A leave of absence allows you to step away from your duties for a period of time. To request one, simply fill the form to your left, and enter the reason for which you're stepping away. You will also need to specify when you will be unavailable, and when you predict to be back.": "", "A leave of absence is a time period in which an employee takes personal time off, for a multitude of reasons. It's a prolonged, authorized absence form work and\/or other duties, communicated in advance, usually via letter or via an HR system.": "", "All Applications": "", @@ -75,6 +77,7 @@ "Applicant IP Address": "", "Applicant Name": "", "Application": "", + "Application-specific commands": "", "Application access denied": "", "Application Date": "", "Application deleted. Comments, appointments and responses have also been deleted.": "", @@ -82,6 +85,8 @@ "Application form management tool": "", "Application ID": "", "Application Management": "", + "Application Override: Approve": "", + "Application Override: Decline": "", "Application Process": "", "Applications": "", "Applications Closed": "", @@ -102,6 +107,7 @@ "Are you sure you want to cancel this appointment? The user will be notified of this via email, and you will be able to reschedule.": "", "Are you sure you want to delete this log file: :date ?": "", "Are you sure you want to DELETE this log file: :date ?": "", + "Are you sure you want to delete your profile?": "", "Are you sure you want to deny this application? Please keep in mind that this user will only be allowed to apply 30 days after their first application.": "", "Are you sure you want to leave the form builder? You have unsaved work.": "", "Are you sure you want to submit your application? Please review each of your answers carefully before doing so.": "", @@ -140,6 +146,7 @@ "Choose a field type": "", "Choose a game in the section below, if applicable.": "", "Choose an application": "", + "Choose an application to override": "", "Choose a security policy": "", "Choose a user to invite": "", "Choose File (max. :maxFileSizeSettingValue)": "", @@ -169,11 +176,14 @@ "Context": "", "Contextual information": "", "Continue": "", + "Copyright © :currentYear :authorName — :licenseFullName<\/a>": "", "Count all votes now": "", "Counts and processes all backlogged votes, for all applications.": "", "Create": "", "Create a form first, then, create a vacancy.": "", + "Create a profile": "", "Created at": "", + "Create profile": "", "Current Email Address": "", "Current from (uneditable)": "", "Current Password": "", @@ -182,14 +192,17 @@ "Dashboard": "", "Date": "", "Decision & Moderation Tools": "", + "Decline application": "", "Declined": "", "Declined by an admin or withdrawn by the requester": "", "Delete": "", "Delete Account": "", "Delete log file": "", + "Delete Profile": "", "Delete request": "", "Deleting a user's account is an irreversible process. Historic and current applications, votes, and profile content, as well as any personally identifiable information will be immediately erased.": "", "Deleting your account is an irreversible process. The following data will be deleted (including personally identifiable data):": "", + "Deleting your profile is an irreversible operation. You will not be able to recover any previously entered information.": "", "Demo accounts:": "", "Demo mode disables some app features in order to preserve it's integrity for everyone who wants to test it. Here's what's disabled: ": "", "Demo mode is active on this instance. The database is refreshed daily and some features are disabled for security reasons.": "", @@ -208,6 +221,7 @@ "Discord Handle": "", "Discord Role ID": "", "Discord role ID": "", + "Discord tag: :discordTag": "", "Dispatch approval event": "", "Dispatches an approval event for the selected application": "", "Dispatches a rejection event for the selected application": "", @@ -222,8 +236,10 @@ "Duration": "", "e.g. Spamming": "", "Edit": "", + "Edit account": "", "Edit Account": "", "Edit form": "", + "Editing :formTitle...": "", "Editor": "", "Edit Team": "", "Edit vacancies": "", @@ -236,6 +252,7 @@ "Ended": "", "entries": "", "ENV": "", + "Error": "", "European?": "", "Event dispatched; Candidate approval sequence initiated.": "", "Event dispatched; Candidate rejection sequence initiated.": "", @@ -288,14 +305,20 @@ "High (╯°□°)╯︵ ┻━┻": "", "Home": "", "Hooray! 2FA is setup correctly for your account. A code will be asked each time you login.": "", + "Housekeeping": "", + "Housekeeping jobs usually run once every day, but if one of them has failed for some reason, you can manually run them here.": "", "How to request a leave of absence": "", "Human Resources": "", + "I am 13 years of age or older and have read and agree with the Community Guidelines<\/a>, Privacy Policy<\/a> and Terms of Service<\/a> set forth.": "", "If an interview is scheduled, you'll need to open your application here and confirm the time, date, and location assigned for you.": "", + "If the setting \"Require Valid Game License\" is activated, editing this field may have unintended consequences. Proceed with caution.": "", "If this form has been updated, new fields and updated questions will not show up here!": "", "If you'd like to learn more about our community, make sure to visit our main website<\/a>!": "", "If you're seeing this message in error, please contact your system administrator.": "", + "If you change your mind and want your profile back, you will be able to create a new blank profile from your profile configuration page, restoring access to the features mentioned above.": "", "If you weren't present during this meeting, you can view the shared meeting notepad to help you make a decision.": "", "Image Describing Access Denied": "", + "In a future update, the public Directory page might become inaccessible to users without a profile.": "", "Incorrect code. Please reopen the 2FA settings panel and try again.": "", "in days": "", "Ineligible (:days) day(s) remaining": "", @@ -359,6 +382,7 @@ "Mojang Logo (Minecraft)": "", "Mojang UUID (deprecated)": "", "Move to next stage": "", + "Multi-factor authentication is enabled for your account.": "", "Must contain numerical characters": "", "Must contain special characters": "", "Must contain upper and lower case characters": "", @@ -383,6 +407,7 @@ "No": "", "No details yet... Add some!": "", "None yet": "", + "No profile found!": "", "No requests": "", "Not available": "", "Note! The database is wiped every six hours during demo mode.": "", @@ -406,6 +431,7 @@ "Our services are currently undergoing routine maintenance. We are sorry for any inconveniences caused! We'll be back ASAP.": "", "Outstanding": "", "Outstanding (Submitted)": "", + "Override now": "", "Page :current of :last": "", "Password": "", "Password change": "", @@ -419,6 +445,7 @@ "permanently.": "", "Permission group": "", "Platform": "", + "Please accept the Community Guidelines, Terms of Service and Privacy Policy to continue.": "", "Please allow up to three days for your application to be processed. Your application will be reviewed by every team member, and will move up in stages.": "", "Please authenticate": "", "Please change update your password now. You won't be able to use the site until you do this.": "", @@ -430,6 +457,7 @@ "Please confirm that you want to suspend this account. You'll need to add a reason and expiration date to confirm this.": "", "Please contact your administrator if you believe this was in error.": "", "Please do not tamper with the URLs. To report a bug, please contact an administrator.": "", + "Please enter a valid (and Premium) Minecraft username! We do not support cracked users.": "", "Please fill out the form below. Keep all answers concise and complete. Please keep in mind that the age requirement is at least :ageUpperLimitSettingValue years old.": "", "Please note: Applications CANNOT be modified once they're submitted!": "", "Please type the above text": "", @@ -446,6 +474,7 @@ "Private": "", "Privilege editing": "", "Profile": "", + "Profile deleted successfully.": "", "Profile updated.": "", "Public note": "", "Public Team": "", @@ -479,6 +508,9 @@ "Reviewer": "", "Reviewing admin": "", "Roles": "", + "Run task: end expired absence requests": "", + "Run task: lift expired suspensions": "", + "Run task: process pending votes": "", "Same as Medium, but: ": "", "Save & Quit": "", "Save and Close": "", @@ -510,6 +542,7 @@ "Sign-in": "", "Sign in": "", "Sign in here": "", + "Sign in to your account": "", "Sign out": "", "Sign up": "", "Sign up for an account": "", @@ -534,6 +567,7 @@ "Submit for approval": "", "Submitted": "", "Submitted at": "", + "Success": "", "Successfully logged out other devices. Remember to change your password if you think you've been compromised.": "", "Supported apps you can install:": "", "Suspend": "", @@ -576,6 +610,7 @@ "The password is \":demoPassword\" for all accounts.": "", "The permission group from your server\/network's permissions manager. Compatible with Luckperms and PEX (This feature is deprecated and will be removed on a future version).": "", "The position you're trying to update doesn't exist!": "", + "There are many benefits to creating a profile! For instance, you'll be able to set a profile picture, as well as sharing your social media and anything else you'd like. Creating a profile is instant and you can begin configuring it right away. If you later change your mind, you may also delete your profile at any time.": "", "There are no applications here": "", "There are no comments here! Comments are only visible to staff members. Be the first to share your input! Commenting may help with decision-making when time comes to vote for an application.": "", "There are no notes yet. Add some!": "", @@ -588,6 +623,7 @@ "There was an error deleting the file: :msg": "", "There were :usersCount user(s) matching your search.": "", "There were no expired suspensions (or no suspensions at all) to purge.": "", + "These tools were intended for development purposes. Unless you know exactly what each command does, we recommend you don't use any of them.": "", "The vacancies you select determine what applications your team members see. All applications under the vacancies you choose will be displayed.": "", "This account has been suspended :suspensionTypeValue": "", "This account isn't suspended!": "", @@ -599,10 +635,12 @@ "This is how your form looks like to applicants.": "", "This is the name team members will see.": "", "This opening does not have any details yet.": "", + "This panel allows you to override statuses for specific applications. Overriding them will trigger the correct events as well. Note that this system entirely ignores the voting system because these statuses ignore all other logic.": "", "THIS PROCESS IS IRREVERSIBLE AND IMMEDIATE": "", "This query didn't return any results.": "", "This request reached its predicted end date": "", "This setting controls whether people can join the team freely.": "", + "This user has been suspended.": "", "This user has been suspended by the admins. Admins suspend accounts for a variety of reasons, including spam.": "", "This user will receive an email notification asking them to join your team.": "", "This vacancy does not have any details yet.": "", @@ -638,6 +676,7 @@ "User invited successfully!": "", "User profile picture": "", "Users": "", + "Users \/ Accounts \/ Admin": "", "Users will be locked out after this time period if they fail to enable 2FA. Leave empty to disable.": "", "User terminated successfully.": "", "User updated successfully!": "", @@ -682,6 +721,7 @@ "Will you be available to assist occasionally during your absence?": "", "Wrong confirmation text! Try again.": "", "Yes": "", + "Yes, delete my profile": "", "You're looking at all applications ever received": "", "You're not authorized to access this page.": "", "You've been redirected here because your password has expired. All users must change their password every :numDaysChangePw days. This is put in place to make sure user accounts remain secure.": "", @@ -703,8 +743,10 @@ "You may only have one active request at the same time, which will have to be either approved or declined by the admins. Please keep in mind that you will not be able to delete any of your requests.": "", "You may vote on as many applications as needed; However, you can only vote once per application.": "", "You need to be authenticated to access this page. Believe this is a mistake? Contact us and let us know! ": "", + "Your account currently has no profile on file. This means that your account will not be visibile in the public profile Directory, and you will not be able to access your profile until you create one.": "", "Your account is not permitted to submit another application. Please wait :applicationThrottleLimitSettingValue more days before trying to submit an application.": "", "Your account will be locked during this process.": "", + "Your account will no longer be listed on the public Directory page.": "", "Your comments": "", "Your current password security policy is set to off<\/b>. This allows users to choose potentially unsafe passwords. We strongly recommend you update this value to Medium<\/b>.": "", "Your current session: logged in from :ipAddress": "", @@ -713,12 +755,16 @@ "Your message": "", "Your password has expired": "", "Your previous applications": "", + "Your profile configuration page will become inaccessible with an error message.": "", "Your profile data and preferences": "", + "Your profile has been created.": "", + "Your profile page will become inaccesible to everyone with an error message.": "", "Your request": "", "Your roles": "", "Your search term did not return any results.": "", "Your upcoming interviews": "", "Your vote has been counted!": "", "You will also need to agree to the terms of a LOA. Additionally, you may also specify whether you'll be available to chat occasionally during your absence, but not perform your full duties.": "", - "Zoom": "" + "Zoom": "", + "{1} There is :count open position!|[2,*] There are :count open positions!": "" } \ No newline at end of file diff --git a/lang/pt-br/adminlte::adminlte.php b/lang/pt-br/adminlte::adminlte.php old mode 100644 new mode 100755 diff --git a/lang/pt-br/log-viewer::general.php b/lang/pt-br/log-viewer::general.php old mode 100644 new mode 100755 diff --git a/lang/pt-br/messages.php b/lang/pt-br/messages.php deleted file mode 100644 index 4cd398a..0000000 --- a/lang/pt-br/messages.php +++ /dev/null @@ -1,28 +0,0 @@ - '', - 'choose_app' => '', - 'dashboard' => '', - 'details_m_title' => '', - 'dispatch_event' => '', - 'edt_action' => '', - 'global_error' => '', - 'global_success' => '', - 'modal_close' => '', - 'open' => '', - 'ppolicy' => '', - 'profile' => [ - 'discord_tag' => '', - 'edituser' => '', - 'edituser_consequence' => '', - ], - 'reusable' => [ - 'confirm' => '', - ], - 'signin_cta' => '', - 'terms' => '', - 'view_app' => [ - 'denyapp' => '', - ], -]; diff --git a/lang/pt-br/pagination.php b/lang/pt-br/pagination.php old mode 100644 new mode 100755 diff --git a/lang/pt-br/passwords.php b/lang/pt-br/passwords.php new file mode 100755 index 0000000..fb0e6b8 --- /dev/null +++ b/lang/pt-br/passwords.php @@ -0,0 +1,9 @@ + 'Your password has been reset!', + 'sent' => 'We have emailed your password reset link!', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => 'We can\'t find a user with that email address.', +]; diff --git a/lang/pt-br/validation.php b/lang/pt-br/validation.php new file mode 100755 index 0000000..b3eaf74 --- /dev/null +++ b/lang/pt-br/validation.php @@ -0,0 +1,115 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'attributes' => [ + ], + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute must have between :min and :max items.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute must be between :min and :max.', + 'string' => 'The :attribute must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute must have more than :value items.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'numeric' => 'The :attribute must be greater than :value.', + 'string' => 'The :attribute must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute must have :value items or more.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => 'The :attribute must be an integer.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'array' => 'The :attribute must have less than :value items.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'numeric' => 'The :attribute must be less than :value.', + 'string' => 'The :attribute must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute must not have more than :value items.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'numeric' => 'The :attribute must be less than or equal :value.', + 'string' => 'The :attribute must be less than or equal :value characters.', + ], + 'max' => [ + 'array' => 'The :attribute may not have more than :max items.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'numeric' => 'The :attribute may not be greater than :max.', + 'string' => 'The :attribute may not be greater than :max characters.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute must have at least :min items.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'numeric' => 'The :attribute must be at least :min.', + 'string' => 'The :attribute must be at least :min characters.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'array' => 'The :attribute must contain :size items.', + 'file' => 'The :attribute must be :size kilobytes.', + 'numeric' => 'The :attribute must be :size.', + 'string' => 'The :attribute must be :size characters.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', +]; diff --git a/lang/pt.json b/lang/pt.json old mode 100644 new mode 100755 index ece2638..149ddd7 --- a/lang/pt.json +++ b/lang/pt.json @@ -39,6 +39,7 @@ "Account": "", "Account already suspended!": "", "Account deletion": "", + "Account Management": "", "Account management (admin)": "", "Account Security": "", "Account Settings": "", @@ -57,6 +58,7 @@ "Administration": "", "Administrative actions such as:": "", "Admin logs": "", + "After you delete your profile, the following will happen:": "", "A leave of absence allows you to step away from your duties for a period of time. To request one, simply fill the form to your left, and enter the reason for which you're stepping away. You will also need to specify when you will be unavailable, and when you predict to be back.": "", "A leave of absence is a time period in which an employee takes personal time off, for a multitude of reasons. It's a prolonged, authorized absence form work and\/or other duties, communicated in advance, usually via letter or via an HR system.": "", "All Applications": "", @@ -75,6 +77,7 @@ "Applicant IP Address": "", "Applicant Name": "", "Application": "", + "Application-specific commands": "", "Application access denied": "", "Application Date": "", "Application deleted. Comments, appointments and responses have also been deleted.": "", @@ -82,6 +85,8 @@ "Application form management tool": "", "Application ID": "", "Application Management": "", + "Application Override: Approve": "", + "Application Override: Decline": "", "Application Process": "", "Applications": "", "Applications Closed": "", @@ -102,6 +107,7 @@ "Are you sure you want to cancel this appointment? The user will be notified of this via email, and you will be able to reschedule.": "", "Are you sure you want to delete this log file: :date ?": "", "Are you sure you want to DELETE this log file: :date ?": "", + "Are you sure you want to delete your profile?": "", "Are you sure you want to deny this application? Please keep in mind that this user will only be allowed to apply 30 days after their first application.": "", "Are you sure you want to leave the form builder? You have unsaved work.": "", "Are you sure you want to submit your application? Please review each of your answers carefully before doing so.": "", @@ -140,6 +146,7 @@ "Choose a field type": "", "Choose a game in the section below, if applicable.": "", "Choose an application": "", + "Choose an application to override": "", "Choose a security policy": "", "Choose a user to invite": "", "Choose File (max. :maxFileSizeSettingValue)": "", @@ -169,11 +176,14 @@ "Context": "", "Contextual information": "", "Continue": "", + "Copyright © :currentYear :authorName — :licenseFullName<\/a>": "", "Count all votes now": "", "Counts and processes all backlogged votes, for all applications.": "", "Create": "", "Create a form first, then, create a vacancy.": "", + "Create a profile": "", "Created at": "", + "Create profile": "", "Current Email Address": "", "Current from (uneditable)": "", "Current Password": "", @@ -182,14 +192,17 @@ "Dashboard": "", "Date": "", "Decision & Moderation Tools": "", + "Decline application": "", "Declined": "", "Declined by an admin or withdrawn by the requester": "", "Delete": "", "Delete Account": "", "Delete log file": "", + "Delete Profile": "", "Delete request": "", "Deleting a user's account is an irreversible process. Historic and current applications, votes, and profile content, as well as any personally identifiable information will be immediately erased.": "", "Deleting your account is an irreversible process. The following data will be deleted (including personally identifiable data):": "", + "Deleting your profile is an irreversible operation. You will not be able to recover any previously entered information.": "", "Demo accounts:": "", "Demo mode disables some app features in order to preserve it's integrity for everyone who wants to test it. Here's what's disabled: ": "", "Demo mode is active on this instance. The database is refreshed daily and some features are disabled for security reasons.": "", @@ -208,6 +221,7 @@ "Discord Handle": "", "Discord Role ID": "", "Discord role ID": "", + "Discord tag: :discordTag": "", "Dispatch approval event": "", "Dispatches an approval event for the selected application": "", "Dispatches a rejection event for the selected application": "", @@ -222,8 +236,10 @@ "Duration": "", "e.g. Spamming": "", "Edit": "", + "Edit account": "", "Edit Account": "", "Edit form": "", + "Editing :formTitle...": "", "Editor": "", "Edit Team": "", "Edit vacancies": "", @@ -236,6 +252,7 @@ "Ended": "", "entries": "", "ENV": "", + "Error": "", "European?": "", "Event dispatched; Candidate approval sequence initiated.": "", "Event dispatched; Candidate rejection sequence initiated.": "", @@ -288,14 +305,20 @@ "High (╯°□°)╯︵ ┻━┻": "", "Home": "", "Hooray! 2FA is setup correctly for your account. A code will be asked each time you login.": "", + "Housekeeping": "", + "Housekeeping jobs usually run once every day, but if one of them has failed for some reason, you can manually run them here.": "", "How to request a leave of absence": "", "Human Resources": "", + "I am 13 years of age or older and have read and agree with the Community Guidelines<\/a>, Privacy Policy<\/a> and Terms of Service<\/a> set forth.": "", "If an interview is scheduled, you'll need to open your application here and confirm the time, date, and location assigned for you.": "", + "If the setting \"Require Valid Game License\" is activated, editing this field may have unintended consequences. Proceed with caution.": "", "If this form has been updated, new fields and updated questions will not show up here!": "", "If you'd like to learn more about our community, make sure to visit our main website<\/a>!": "", "If you're seeing this message in error, please contact your system administrator.": "", + "If you change your mind and want your profile back, you will be able to create a new blank profile from your profile configuration page, restoring access to the features mentioned above.": "", "If you weren't present during this meeting, you can view the shared meeting notepad to help you make a decision.": "", "Image Describing Access Denied": "", + "In a future update, the public Directory page might become inaccessible to users without a profile.": "", "Incorrect code. Please reopen the 2FA settings panel and try again.": "", "in days": "", "Ineligible (:days) day(s) remaining": "", @@ -359,6 +382,7 @@ "Mojang Logo (Minecraft)": "", "Mojang UUID (deprecated)": "", "Move to next stage": "", + "Multi-factor authentication is enabled for your account.": "", "Must contain numerical characters": "", "Must contain special characters": "", "Must contain upper and lower case characters": "", @@ -383,6 +407,7 @@ "No": "", "No details yet... Add some!": "", "None yet": "", + "No profile found!": "", "No requests": "", "Not available": "", "Note! The database is wiped every six hours during demo mode.": "", @@ -406,6 +431,7 @@ "Our services are currently undergoing routine maintenance. We are sorry for any inconveniences caused! We'll be back ASAP.": "", "Outstanding": "", "Outstanding (Submitted)": "", + "Override now": "", "Page :current of :last": "", "Password": "", "Password change": "", @@ -419,6 +445,7 @@ "permanently.": "", "Permission group": "", "Platform": "", + "Please accept the Community Guidelines, Terms of Service and Privacy Policy to continue.": "", "Please allow up to three days for your application to be processed. Your application will be reviewed by every team member, and will move up in stages.": "", "Please authenticate": "", "Please change update your password now. You won't be able to use the site until you do this.": "", @@ -430,6 +457,7 @@ "Please confirm that you want to suspend this account. You'll need to add a reason and expiration date to confirm this.": "", "Please contact your administrator if you believe this was in error.": "", "Please do not tamper with the URLs. To report a bug, please contact an administrator.": "", + "Please enter a valid (and Premium) Minecraft username! We do not support cracked users.": "", "Please fill out the form below. Keep all answers concise and complete. Please keep in mind that the age requirement is at least :ageUpperLimitSettingValue years old.": "", "Please note: Applications CANNOT be modified once they're submitted!": "", "Please type the above text": "", @@ -446,6 +474,7 @@ "Private": "", "Privilege editing": "", "Profile": "", + "Profile deleted successfully.": "", "Profile updated.": "", "Public note": "", "Public Team": "", @@ -479,6 +508,9 @@ "Reviewer": "", "Reviewing admin": "", "Roles": "", + "Run task: end expired absence requests": "", + "Run task: lift expired suspensions": "", + "Run task: process pending votes": "", "Same as Medium, but: ": "", "Save & Quit": "", "Save and Close": "", @@ -510,6 +542,7 @@ "Sign-in": "", "Sign in": "", "Sign in here": "", + "Sign in to your account": "", "Sign out": "", "Sign up": "", "Sign up for an account": "", @@ -534,6 +567,7 @@ "Submit for approval": "", "Submitted": "", "Submitted at": "", + "Success": "", "Successfully logged out other devices. Remember to change your password if you think you've been compromised.": "", "Supported apps you can install:": "", "Suspend": "", @@ -576,6 +610,7 @@ "The password is \":demoPassword\" for all accounts.": "", "The permission group from your server\/network's permissions manager. Compatible with Luckperms and PEX (This feature is deprecated and will be removed on a future version).": "", "The position you're trying to update doesn't exist!": "", + "There are many benefits to creating a profile! For instance, you'll be able to set a profile picture, as well as sharing your social media and anything else you'd like. Creating a profile is instant and you can begin configuring it right away. If you later change your mind, you may also delete your profile at any time.": "", "There are no applications here": "", "There are no comments here! Comments are only visible to staff members. Be the first to share your input! Commenting may help with decision-making when time comes to vote for an application.": "", "There are no notes yet. Add some!": "", @@ -588,6 +623,7 @@ "There was an error deleting the file: :msg": "", "There were :usersCount user(s) matching your search.": "", "There were no expired suspensions (or no suspensions at all) to purge.": "", + "These tools were intended for development purposes. Unless you know exactly what each command does, we recommend you don't use any of them.": "", "The vacancies you select determine what applications your team members see. All applications under the vacancies you choose will be displayed.": "", "This account has been suspended :suspensionTypeValue": "", "This account isn't suspended!": "", @@ -599,10 +635,12 @@ "This is how your form looks like to applicants.": "", "This is the name team members will see.": "", "This opening does not have any details yet.": "", + "This panel allows you to override statuses for specific applications. Overriding them will trigger the correct events as well. Note that this system entirely ignores the voting system because these statuses ignore all other logic.": "", "THIS PROCESS IS IRREVERSIBLE AND IMMEDIATE": "", "This query didn't return any results.": "", "This request reached its predicted end date": "", "This setting controls whether people can join the team freely.": "", + "This user has been suspended.": "", "This user has been suspended by the admins. Admins suspend accounts for a variety of reasons, including spam.": "", "This user will receive an email notification asking them to join your team.": "", "This vacancy does not have any details yet.": "", @@ -638,6 +676,7 @@ "User invited successfully!": "", "User profile picture": "", "Users": "", + "Users \/ Accounts \/ Admin": "", "Users will be locked out after this time period if they fail to enable 2FA. Leave empty to disable.": "", "User terminated successfully.": "", "User updated successfully!": "", @@ -682,6 +721,7 @@ "Will you be available to assist occasionally during your absence?": "", "Wrong confirmation text! Try again.": "", "Yes": "", + "Yes, delete my profile": "", "You're looking at all applications ever received": "", "You're not authorized to access this page.": "", "You've been redirected here because your password has expired. All users must change their password every :numDaysChangePw days. This is put in place to make sure user accounts remain secure.": "", @@ -703,8 +743,10 @@ "You may only have one active request at the same time, which will have to be either approved or declined by the admins. Please keep in mind that you will not be able to delete any of your requests.": "", "You may vote on as many applications as needed; However, you can only vote once per application.": "", "You need to be authenticated to access this page. Believe this is a mistake? Contact us and let us know! ": "", + "Your account currently has no profile on file. This means that your account will not be visibile in the public profile Directory, and you will not be able to access your profile until you create one.": "", "Your account is not permitted to submit another application. Please wait :applicationThrottleLimitSettingValue more days before trying to submit an application.": "", "Your account will be locked during this process.": "", + "Your account will no longer be listed on the public Directory page.": "", "Your comments": "", "Your current password security policy is set to off<\/b>. This allows users to choose potentially unsafe passwords. We strongly recommend you update this value to Medium<\/b>.": "", "Your current session: logged in from :ipAddress": "", @@ -713,12 +755,16 @@ "Your message": "", "Your password has expired": "", "Your previous applications": "", + "Your profile configuration page will become inaccessible with an error message.": "", "Your profile data and preferences": "", + "Your profile has been created.": "", + "Your profile page will become inaccesible to everyone with an error message.": "", "Your request": "", "Your roles": "", "Your search term did not return any results.": "", "Your upcoming interviews": "", "Your vote has been counted!": "", "You will also need to agree to the terms of a LOA. Additionally, you may also specify whether you'll be available to chat occasionally during your absence, but not perform your full duties.": "", - "Zoom": "" + "Zoom": "", + "{1} There is :count open position!|[2,*] There are :count open positions!": "" } \ No newline at end of file diff --git a/lang/pt/adminlte::adminlte.php b/lang/pt/adminlte::adminlte.php old mode 100644 new mode 100755 diff --git a/lang/pt/log-viewer::general.php b/lang/pt/log-viewer::general.php old mode 100644 new mode 100755 diff --git a/lang/pt/messages.php b/lang/pt/messages.php deleted file mode 100644 index 4cd398a..0000000 --- a/lang/pt/messages.php +++ /dev/null @@ -1,28 +0,0 @@ - '', - 'choose_app' => '', - 'dashboard' => '', - 'details_m_title' => '', - 'dispatch_event' => '', - 'edt_action' => '', - 'global_error' => '', - 'global_success' => '', - 'modal_close' => '', - 'open' => '', - 'ppolicy' => '', - 'profile' => [ - 'discord_tag' => '', - 'edituser' => '', - 'edituser_consequence' => '', - ], - 'reusable' => [ - 'confirm' => '', - ], - 'signin_cta' => '', - 'terms' => '', - 'view_app' => [ - 'denyapp' => '', - ], -]; diff --git a/lang/pt/pagination.php b/lang/pt/pagination.php old mode 100644 new mode 100755 diff --git a/lang/pt/passwords.php b/lang/pt/passwords.php new file mode 100755 index 0000000..fb0e6b8 --- /dev/null +++ b/lang/pt/passwords.php @@ -0,0 +1,9 @@ + 'Your password has been reset!', + 'sent' => 'We have emailed your password reset link!', + 'throttled' => 'Please wait before retrying.', + 'token' => 'This password reset token is invalid.', + 'user' => 'We can\'t find a user with that email address.', +]; diff --git a/lang/pt/validation.php b/lang/pt/validation.php new file mode 100755 index 0000000..b3eaf74 --- /dev/null +++ b/lang/pt/validation.php @@ -0,0 +1,115 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'attributes' => [ + ], + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'array' => 'The :attribute must have between :min and :max items.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'numeric' => 'The :attribute must be between :min and :max.', + 'string' => 'The :attribute must be between :min and :max characters.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'array' => 'The :attribute must have more than :value items.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'numeric' => 'The :attribute must be greater than :value.', + 'string' => 'The :attribute must be greater than :value characters.', + ], + 'gte' => [ + 'array' => 'The :attribute must have :value items or more.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => 'The :attribute must be an integer.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'array' => 'The :attribute must have less than :value items.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'numeric' => 'The :attribute must be less than :value.', + 'string' => 'The :attribute must be less than :value characters.', + ], + 'lte' => [ + 'array' => 'The :attribute must not have more than :value items.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'numeric' => 'The :attribute must be less than or equal :value.', + 'string' => 'The :attribute must be less than or equal :value characters.', + ], + 'max' => [ + 'array' => 'The :attribute may not have more than :max items.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'numeric' => 'The :attribute may not be greater than :max.', + 'string' => 'The :attribute may not be greater than :max characters.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'array' => 'The :attribute must have at least :min items.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'numeric' => 'The :attribute must be at least :min.', + 'string' => 'The :attribute must be at least :min characters.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'array' => 'The :attribute must contain :size items.', + 'file' => 'The :attribute must be :size kilobytes.', + 'numeric' => 'The :attribute must be :size.', + 'string' => 'The :attribute must be :size characters.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', +]; diff --git a/package-lock.json b/package-lock.json old mode 100644 new mode 100755 diff --git a/package.json b/package.json old mode 100644 new mode 100755 diff --git a/phpunit.xml b/phpunit.xml index 964ff0c..4944782 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -21,7 +21,6 @@ - diff --git a/public/css/login.css b/public/css/login.css index 0631eca..e546cd2 100755 --- a/public/css/login.css +++ b/public/css/login.css @@ -77,5 +77,16 @@ body { .login-card-footer-nav a { font-size: 14px; color: #919aa3; } +.btn-discord { + color: #FFFFFF; + background-color: #5865f2; + border-radius: 20px; +} + +.btn-discord:hover { + border-color: #5865f2; + border-width: 1px; + background-color: #FFFFFF !important; +} /*# sourceMappingURL=login.css.map */ diff --git a/public/img/401.svg b/public/img/401.svg old mode 100644 new mode 100755 diff --git a/public/img/404.svg b/public/img/404.svg old mode 100644 new mode 100755 diff --git a/public/img/500.svg b/public/img/500.svg old mode 100644 new mode 100755 diff --git a/public/img/503.svg b/public/img/503.svg old mode 100644 new mode 100755 diff --git a/public/img/GitHub-Mark-32px.png b/public/img/GitHub-Mark-32px.png new file mode 100755 index 0000000..8b25551 Binary files /dev/null and b/public/img/GitHub-Mark-32px.png differ diff --git a/public/img/gplv3-with-text-84x42.png b/public/img/gplv3-with-text-84x42.png new file mode 100755 index 0000000..aa88ab6 Binary files /dev/null and b/public/img/gplv3-with-text-84x42.png differ diff --git a/public/img/small_logo_blurple_RGB.svg b/public/img/small_logo_blurple_RGB.svg new file mode 100644 index 0000000..1bc7f8f --- /dev/null +++ b/public/img/small_logo_blurple_RGB.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/js/app.js.LICENSE.txt b/public/js/app.js.LICENSE.txt index ae59d3a..37b9d7d 100755 --- a/public/js/app.js.LICENSE.txt +++ b/public/js/app.js.LICENSE.txt @@ -7,9 +7,9 @@ */ /*! - * Chart.js v2.9.3 + * Chart.js v2.9.4 * https://www.chartjs.org - * (c) 2019 Chart.js Contributors + * (c) 2020 Chart.js Contributors * Released under the MIT License */ diff --git a/resources/views/auth/2fa.blade.php b/resources/views/auth/2fa.blade.php index d29e6cc..6c005ac 100755 --- a/resources/views/auth/2fa.blade.php +++ b/resources/views/auth/2fa.blade.php @@ -13,7 +13,7 @@
{{ config('adminlte.logo') }}
- +
@csrf
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index b01b1b2..8b31fe4 100755 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -13,7 +13,7 @@
{{ config('adminlte.logo') }}
- +
@csrf @if ($demoActive) @@ -45,13 +45,17 @@ +

{{ __('OR') }}

+
{{__('Forgot password?')}} diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php index a193902..202dcda 100755 --- a/resources/views/auth/passwords/email.blade.php +++ b/resources/views/auth/passwords/email.blade.php @@ -31,9 +31,9 @@ {{__('Back to login')}} diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php index 70f3faf..1bca8fc 100755 --- a/resources/views/auth/passwords/reset.blade.php +++ b/resources/views/auth/passwords/reset.blade.php @@ -70,9 +70,9 @@ {{__('Back to sign in')}} diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 98c92fc..e993c46 100755 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -72,11 +72,16 @@ -
+
+
+ + + {!! __("Why do we need this? We use your age information to make sure you meet age requirements for certain positions, and to make sure that everyone is compliant with our terms of service.") !!} +
@if(\App\Facades\Options::getOption('requireGameLicense') && \App\Facades\Options::getOption('currentGame') == 'MINECRAFT')
@@ -85,13 +90,26 @@
@endif +
+ +
+ + + +
diff --git a/resources/views/auth/verify.blade.php b/resources/views/auth/verify.blade.php index 703368a..279e717 100755 --- a/resources/views/auth/verify.blade.php +++ b/resources/views/auth/verify.blade.php @@ -30,9 +30,9 @@ diff --git a/resources/views/breadcrumbs/app.blade.php b/resources/views/breadcrumbs/app.blade.php index 7914acd..87108cb 100755 --- a/resources/views/breadcrumbs/app.blade.php +++ b/resources/views/breadcrumbs/app.blade.php @@ -9,13 +9,13 @@ @if (session()->has('error')) @elseif (session()->has('success')) @endif diff --git a/resources/views/breadcrumbs/auth/main.blade.php b/resources/views/breadcrumbs/auth/main.blade.php index a0dca99..81caf7a 100755 --- a/resources/views/breadcrumbs/auth/main.blade.php +++ b/resources/views/breadcrumbs/auth/main.blade.php @@ -9,7 +9,11 @@ + + + + diff --git a/resources/views/breadcrumbs/dashboard/footer.blade.php b/resources/views/breadcrumbs/dashboard/footer.blade.php index 34a2cf2..e09b23d 100755 --- a/resources/views/breadcrumbs/dashboard/footer.blade.php +++ b/resources/views/breadcrumbs/dashboard/footer.blade.php @@ -3,20 +3,15 @@
-

© Miguel N. {{ \Carbon\Carbon::now()->year }}— {{ __('GNU General Public License') }}

+

{!! __('Copyright © :currentYear :authorName — :licenseFullName', ['authorName' => 'Miguel Nogueira', 'currentYear' => now()->year, 'licenseTextURL' => 'https://www.gnu.org/licenses/gpl-3.0.en.html', 'licenseFullName' => 'GNU GPL v3']) !!}

diff --git a/resources/views/breadcrumbs/header.blade.php b/resources/views/breadcrumbs/header.blade.php index a27dee4..22144d3 100755 --- a/resources/views/breadcrumbs/header.blade.php +++ b/resources/views/breadcrumbs/header.blade.php @@ -59,9 +59,6 @@ - - - @@ -113,7 +110,7 @@

{{config('app.name')}}

-
{{ __('Welcome to the Games Club Recruitment Portal!') }}
+
{{ __('Welcome to the :appName Recruitment Portal!', ['appName' => config('app.name')]) }}

{{ __('We process applications for our Discord server\'s management team here. If you have any questions, don\'t hesistate to contact our support team! Take a look at the open jobs below.') }}

{!! __('If you\'d like to learn more about our community, make sure to visit our main website!', ['mainWebsiteUrlConfigValue' => config('app.sitehomepage')]) !!}

diff --git a/resources/views/components/account-status.blade.php b/resources/views/components/account-status.blade.php index 5618a0e..fd32235 100755 --- a/resources/views/components/account-status.blade.php +++ b/resources/views/components/account-status.blade.php @@ -1,23 +1,41 @@ - @if ($user->isBanned()) + @if ($isSuspended) {{__('Suspended')}} @else {{__('Active')}} @endif @if (Auth::user()->hasRole('admin')) - @if ($user->has2FA()) - {{ __('MFA Active') }} - @else - {{ __('MFA Inactive') }} - @endif + @if ($isLocked) + {{ __('Admin locked') }} + @endif - @if(!is_null($user->email_verified_at)) - {{ __('Verified Email') }} - @else - {{ __('Unverified Email') }} - @endif + @if($isVerified) + {{ __('Verified Email') }} + @else + {{ __('Unverified Email') }} + @endif + + @if($hasPassword) + {{ __('Uses password') }} + @elseif($hasDiscord) + {{ __('Passwordless signin') }} + @else + {{ __('No password') }} + @endif + + @if($hasDiscord) + {{ __('Connected to Discord') }} + @else + {{ __('Disconnected from Discord') }} + @endif + + @if ($has2FA) + {{ __('MFA Active') }} + @else + {{ __('MFA Inactive') }} + @endif @endif diff --git a/resources/views/components/confirm-password.blade.php b/resources/views/components/confirm-password.blade.php new file mode 100755 index 0000000..b2c7dfb --- /dev/null +++ b/resources/views/components/confirm-password.blade.php @@ -0,0 +1,5 @@ +
+ + +

{{ $slot }} {{ __('Forgot your password?') }}

+
diff --git a/resources/views/components/confirm-second-factor.blade.php b/resources/views/components/confirm-second-factor.blade.php new file mode 100755 index 0000000..acfd9e1 --- /dev/null +++ b/resources/views/components/confirm-second-factor.blade.php @@ -0,0 +1,9 @@ +@if (Auth::user()->has2FA()) +
+ + + +

{{ $slot }}

+ +
+@endif diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php index ccbcb52..f638070 100755 --- a/resources/views/components/modal.blade.php +++ b/resources/views/components/modal.blade.php @@ -14,10 +14,10 @@
diff --git a/resources/views/dashboard/administration/devtools.blade.php b/resources/views/dashboard/administration/devtools.blade.php index 1d2508b..efa6fad 100755 --- a/resources/views/dashboard/administration/devtools.blade.php +++ b/resources/views/dashboard/administration/devtools.blade.php @@ -14,7 +14,7 @@ @section('content') - +

{{__('Please choose an application to force approve')}}

{{ __('Note that this process overrides users\'s votes.') }}

@@ -35,7 +35,7 @@ - +
@@ -70,45 +70,64 @@
- {{__('Warning')}} + +

{{__('These tools were intended for development purposes. Unless you know exactly what each command does, we recommend you don\'t use any of them.')}}

-
- -
- - +
+
+ - - - - - -
- @csrf - -
- -
- @csrf - @method('DELETE') - -
+
+ + +
-

.

+

{{ __('This panel allows you to override statuses for specific applications. Overriding them will trigger the correct events as well. Note that this system entirely ignores the voting system because these statuses ignore all other logic.') }}

-
+
+
+ + + + + +
+
+ @csrf + +
+ +
+ @csrf + @method('DELETE') + +
+ +
+ @csrf + @method('DELETE') + +
+
+ + +

{{ __('Housekeeping jobs usually run once every day, but if one of them has failed for some reason, you can manually run them here.') }}

+
+ +
+
@stop diff --git a/resources/views/dashboard/administration/editform.blade.php b/resources/views/dashboard/administration/editform.blade.php index 264b48c..a7df65b 100755 --- a/resources/views/dashboard/administration/editform.blade.php +++ b/resources/views/dashboard/administration/editform.blade.php @@ -47,7 +47,7 @@
-

{{__('messages.edt_action')}} {{ $title }}...

+

{{__('Editing :formTitle...', ['formTitle' => $title])}}

diff --git a/resources/views/dashboard/administration/editposition.blade.php b/resources/views/dashboard/administration/editposition.blade.php index b3b553f..683682d 100755 --- a/resources/views/dashboard/administration/editposition.blade.php +++ b/resources/views/dashboard/administration/editposition.blade.php @@ -93,15 +93,7 @@
- - - - -
- -
- - + @@ -118,6 +110,18 @@ + + + + +
+ + requiresDiscord) checked @endif> + +

{{ __('This will redirect users to sign in with Discord before they are allowed to submit the form.') }}

+
diff --git a/resources/views/dashboard/administration/positions.blade.php b/resources/views/dashboard/administration/positions.blade.php index c15bfe9..80e3042 100755 --- a/resources/views/dashboard/administration/positions.blade.php +++ b/resources/views/dashboard/administration/positions.blade.php @@ -65,13 +65,8 @@
- - -
- -
- - Appearance -> Advanced and toggle Developer Mode. On your server's roles tab, right click any role to copy it's ID.")}}" data-toggle="tooltip" data-placement="bottom" type="text" id="discordrole" name="discordRole" class="form-control"> + +
@@ -93,7 +88,17 @@ + + + +
+ + + +
diff --git a/resources/views/dashboard/administration/players.blade.php b/resources/views/dashboard/administration/users.blade.php similarity index 93% rename from resources/views/dashboard/administration/players.blade.php rename to resources/views/dashboard/administration/users.blade.php index 5fe5f91..d0468f5 100755 --- a/resources/views/dashboard/administration/players.blade.php +++ b/resources/views/dashboard/administration/users.blade.php @@ -78,8 +78,7 @@ {{__('Name')}} - {{ __('Rank') }} - {{__('Status')}} + {{ __('Role') }} {{__('Registration date')}} {{__('Actions')}} @@ -98,12 +97,10 @@ {{ __('Member') }} @endif - - - {{$user->created_at}} - + + {{ __('Manage') }} diff --git a/resources/views/dashboard/application-rendering/add-age.blade.php b/resources/views/dashboard/application-rendering/add-age.blade.php new file mode 100755 index 0000000..85c82c0 --- /dev/null +++ b/resources/views/dashboard/application-rendering/add-age.blade.php @@ -0,0 +1,92 @@ +@extends('adminlte::page') + +@section('title', config('app.name') . ' | ' . __('Account age update')) + +@section('content_header') +

{{__('My account')}} / {{__('Apply')}} / {{__('Account age update')}}

+@stop + +@section('js') + + @if (session()->has('success')) + + + + @elseif(session()->has('error')) + + + + @endif + + @if(!$isEligibleForApplication) + + + + @endif + + + +@stop + +@section('content') + + +
+
+ + {{ __('Because you signed up using Discord, your age was not registered in our system. In order to continue applying for this position, please add your date of birth below. Please note that we don\'t accept registrations from users under 13 years of age.') }} + + + + {{ __('You can\'t change your date of birth after first setting it.') }} + +
+
+ +
+ + + +
+ + + + +
+ @csrf + @method('PATCH') +
+ + + {!! __("Why do we need this? We use your age information to make sure you meet age requirements for certain positions, and to make sure that everyone is compliant with our terms of service.") !!} +
+ + +
+ + + + +
+ + + +
+
+ + +@stop + +@section('footer') + @include('breadcrumbs.dashboard.footer') +@stop diff --git a/resources/views/dashboard/application-rendering/apply.blade.php b/resources/views/dashboard/application-rendering/apply.blade.php index a2d9476..41abb7c 100755 --- a/resources/views/dashboard/application-rendering/apply.blade.php +++ b/resources/views/dashboard/application-rendering/apply.blade.php @@ -80,7 +80,7 @@

{{__('You are applying for: :vacancyNameValue', ['vacancyNameValue' => $vacancy->vacancyName])}}

{{__("We're glad you've decided to apply. Generally, applications take 48 hours to be processed and reviewed. Depending on the circumstances and the volume of applications, you may receive an answer in a shorter time.")}}

-

{{__('Please fill out the form below. Keep all answers concise and complete. Please keep in mind that the age requirement is at least :ageUpperLimitSettingValue years old.', ['ageUpperLimitSettingValue' => '16']) }}.

+

{{__('Please fill out the form below. Keep all answers concise and complete. Please keep in mind that you must be at least :ageUpperLimitSettingValue years old to apply.', ['ageUpperLimitSettingValue' => $vacancy->requiredAge]) }}

{{__('Asking about your application will result in instant denial. Everything you need to know is here.')}}.

{!! __('All fields support Markdown') !!}

@@ -91,6 +91,18 @@
+ @if ($vacancy->requiresDiscord && Auth::user()->hasDiscordConnection()) + +
+ +
+
{!! __(':preIcon Applying as :icon :discordUsername', ['preIcon' => '', 'icon' => '', 'discordUsername' => Auth::user()->username]) !!}
+
+ +
+ + @endif +
diff --git a/resources/views/dashboard/dashboard.blade.php b/resources/views/dashboard/dashboard.blade.php index 6155c8a..47b1934 100755 --- a/resources/views/dashboard/dashboard.blade.php +++ b/resources/views/dashboard/dashboard.blade.php @@ -3,7 +3,7 @@ @section('title', config('app.name')) @section('content_header') -

{{config('app.name')}} / {{__('messages.dashboard')}}

+

{{config('app.name')}} / {{__('Dashboard')}}

@stop @section('js') @@ -67,6 +67,10 @@
@else + @if($vacancy->requiresDiscord) +

{{ __('Note: to apply for this position, you must sign in with your Discord account beforehand.') }}

+ @endif + {!! $vacancy->vacancyFullDescription !!}

{{__('Last updated @ :vacancyUpdatedTimeValue', ['vacancyUpdatedTimeValue' => $vacancy->updated_at]) }} @@ -113,7 +117,7 @@

- {{__('messages.open')}} + {{__('Open')}} @@ -257,7 +261,12 @@ @@ -270,33 +279,6 @@ @endif - - -
- -
- -
- -
- -

-   {{__('Your upcoming interviews')}} ({{__('Coming soon')}}) -

- -
- -
- -
- -
- -
- -
- -
@stop @section('footer') @include('breadcrumbs.dashboard.footer') diff --git a/resources/views/dashboard/user/directory.blade.php b/resources/views/dashboard/user/directory.blade.php deleted file mode 100755 index 81f632b..0000000 --- a/resources/views/dashboard/user/directory.blade.php +++ /dev/null @@ -1,110 +0,0 @@ -@extends('adminlte::page') - -@section('title', config('app.name') . ' | ' . __('User Directory')) - -@section('content_header') - -

{{__('Users')}} / {{__('Directory')}}

- -@stop - -@section('js') - - -@stop - -@section('css') - - - -@stop - - -@section('content') - - @if (Auth::user()->can('profiles.view.others')) - - -
- - @foreach ($users as $user) -
-
-
-

{{ $user->name }}

-
{{ $user->profile->profileShortBio }}
-
- -
- @if($user->profile->avatarPreference == 'gravatar') - {{ __('User profile picture') }} - @else - {{ __('User profile picture') }} - @endif -
- -
- -
- @endforeach - -
- -
- -
- -
- -
- - - -
- -
- -
- -
- - @else -
- -

- {{__("We're sorry, but you do not have permission to access this web page.")}} -

- -
- @endif - -@stop -@section('footer') - @include('breadcrumbs.dashboard.footer') -@stop diff --git a/resources/views/dashboard/user/manage.blade.php b/resources/views/dashboard/user/manage.blade.php new file mode 100755 index 0000000..bebcbed --- /dev/null +++ b/resources/views/dashboard/user/manage.blade.php @@ -0,0 +1,451 @@ +@extends('adminlte::page') + +@section('title', config('app.name') . ' | ' . __('Account Management')) + +@section('content_header') + +

{{ __('Users / Accounts / :username / Manage', ['username' => $user->name]) }}

+ +@stop + +@section('js') + + +@stop + +@section('content') + + @if($user->has2FA()) + + + +

{{ __('Resetting an account\'s two-factor authentication secret will automatically notify the account holder. Additionally, the user\'s password will also be forcefully reset during this process. Please confirm this action by verifying your identity below.') }}

+ +
+ @csrf + @method('PATCH') + + + {{ __('Please re-enter your password.') }} + + + + {{ __('Please enter your two-factor authentication code.') }} + + +
+ + + + + +
+ + @endif + + @if($user->hasPassword()) + + +

{{ __('Forcing a password reset will automatically notify the account holder and send them a password reset link. Please confirm this action by verifying your identity below.') }}

+ +
+ @csrf + @method('patch') + + + {{ __('Re-entering your password is required to confirm sensitive administration actions.') }} + + + + {{ __('Two-factor authentication is required to confirm sensitive administration actions.') }} + + +
+ + + + + +
+ @endif + + + +

{{__("Please confirm that you want to suspend this account. You'll need to add a reason and expiration date to confirm this.")}}

+ +
+ @csrf + + @if($demoActive) +
+

{{ __('This feature is disabled') }}

+
+ @endif + +
+ +
+ + +
+ +
+ + +
+
+ + +
+ + +
+ +

{{ __('Temporary suspensions will be automatically lifted. The suspension note is visible to all users. Suspended users will not be able to login or register.') }}

+
+ + +
+ + + + + +
+ + @if (!Auth::user()->is($user) && $user->isStaffMember()) + + + @if($demoActive) +
+

{{ __('This feature is disabled') }}

+
+ @endif + +

{{__('You are about to terminate a recruited staff member')}}

+

+ {{__('Terminating a staff member will remove their privileges on the application management site and connected integrations configured for the vacancy they applied for.')}} +

+

+ {{__('THIS PROCESS IS IRREVERSIBLE AND IMMEDIATE')}} +

+ + + + +
+ @csrf + @method('PATCH') + + +
+ +
+ +
+ @endif + + + + @if($demoActive) +
+

{{ __('This feature is disabled') }}

+
+ @endif + +

{{__('WARNING: This is a potentially destructive action!')}}

+ +

{{__("Deleting a user's account is an irreversible process. Historic and current applications, votes, and profile content, as well as any personally identifiable information will be immediately erased.")}}

+ +
$user->id])}}> + + @csrf + @method('DELETE') + + + + +
+ + + + + + +
+ + + + + @if($demoActive) +
+

{{ __('This feature is disabled') }}

+
+ @endif + +
+ @csrf + @method('PATCH') + + + + + + + + @if ($requireLicense) + + +

+ {{__('If the setting "Require Valid Game License" is activated, editing this field may have unintended consequences. Proceed with caution.')}} +

+ @endif + +
+ + + + + + @foreach($roles as $roleName => $status) + + + + + + @endforeach + + + + +
{{ ucfirst($roleName) }}
+ +
+ +
+ + + + + + + +
+ + +
+ +
+ +
+ +
+

{{ __('Personal details') }}

+
+ +
+
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ +
+ + +
+ + @if($user->hasDiscordConnection()) +
+
{!! __('Connected to Discord as :tag', ['tag' => $user->username]) !!}
+

{{ __('User ID: :discordUserID', ['discordUserID' => $user->discord_user_id]) }}

+
+ @endif + +
+ + +
+ +
+ +
+ +
+
+ +
+
+

{{ __('Application history') }}

+
+ +
+ @if (!$applications->isEmpty()) + + + + + + + + + + + + + + + + + + @foreach($applications as $application) + + + + + + + + + + @endforeach + + + +
#{{__('Status')}}{{ __('Vacancy') }}{{__('Date')}}{{__('Actions')}}
{{ $application->id }} + @switch($application->applicationStatus) + + @case('STAGE_SUBMITTED') + + {{__('Outstanding (Submitted)')}} + @break + + @case('STAGE_PEERAPPROVAL') + + {{__('Peer Review')}} + @break + + @case('STAGE_INTERVIEW') + + {{__('Interview')}} + + @break + + @case('STAGE_INTERVIEW_SCHEDULED') + + {{__('Interview Scheduled')}} + + @break + + @case('APPROVED') + + {{__('Approved')}} + + @break + + @case('DENIED') + + {{__('Denied')}} + + @break; + + @default + {{__('Denied')}} + + @endswitch + {{ $application->response->vacancy->vacancyName }}{{ $application->created_at }} ({{ $application->created_at->diffForHumans() }}) + + {{ __('View') }} + +
+ @else + + {{ __('This user has not submitted any applications yet.') }} + + @endif +
+
+
+
+ + +
+
+ +
+
+

{{ __('Admin actions') }}

+
+ +
+ + @if ($isSuspended) + +

 {{ __('This account has been :suspensionType suspended.', ['suspensionType' => (is_null($suspensionDuration)) ? __('permanently') : __('temporarily') ]) }}

+

 {!! __('Reason: :reason', ['reason' => $suspensionReason]) !!}

+ @if (!is_null($suspensionDuration)) +

 {!! __('Suspension expires: :duration', ['duration' => $suspensionDuration]) !!}

+ @endif +
+ @endif + + @if (!$isSuspended) + + @else +
+ @csrf + @method('DELETE') + + +
+ @endif + + @if($user->hasPassword()) + + @else + + @endif + + @if($user->has2FA()) + + @endif + +
+
+
+
+ +@stop + + +@section('footer') + @include('breadcrumbs.dashboard.footer') +@stop diff --git a/resources/views/dashboard/user/profile/displayprofile.blade.php b/resources/views/dashboard/user/profile/displayprofile.blade.php index a1a46ea..05c8016 100755 --- a/resources/views/dashboard/user/profile/displayprofile.blade.php +++ b/resources/views/dashboard/user/profile/displayprofile.blade.php @@ -24,7 +24,7 @@ {{__('This account has been suspended :suspensionTypeValue', ['suspensionTypeValue' => ($suspensionInfo['isPermanent']) ? __('permanently.') : __('until :date.', ['date' => $suspensionInfo['bannedUntil']])]) }} -

{{__('This user has been suspended by the admins. Admins suspend accounts for a variety of reasons, including spam.')}}

+

{{__('This user has been suspended.')}}

{{$suspensionInfo['reason']}} @@ -34,256 +34,6 @@ @endif - @if (Auth::user()->hasRole('admin')) - - - -

{{__("Please confirm that you want to suspend this account. You'll need to add a reason and expiration date to confirm this.")}}

- -
- @csrf - - @if($demoActive) -
-

{{ __('This feature is disabled') }}

-
- @endif - -
- -
- - -
- -
- - -
-
- - -
- - -
- -

{{ __('Temporary suspensions will be automatically lifted. The suspension note is visible to all users. Suspended users will not be able to login or register.') }}

-
- - -
- - - - - - - - @if (!Auth::user()->is($profile->user) && $profile->user->isStaffMember()) - - - @if($demoActive) -
-

{{ __('This feature is disabled') }}

-
- @endif - -

{{__('You are about to terminate a recruited staff member')}}

-

- {{__('Terminating a staff member will remove their privileges on the application management site and connected integrations configured for the vacancy they applied for.')}} -

-

- {{__('THIS PROCESS IS IRREVERSIBLE AND IMMEDIATE')}} -

- - - - -
- @csrf - @method('PATCH') - - -
- -
- -
- @endif - - - - @if($demoActive) -
-

{{ __('This feature is disabled') }}

-
- @endif - -

{{__('WARNING: This is a potentially destructive action!')}}

- -

{{__("Deleting a user's account is an irreversible process. Historic and current applications, votes, and profile content, as well as any personally identifiable information will be immediately erased.")}}

- -
$profile->user->id])}}> - - @csrf - @method('DELETE') - - - - -
- - - - - - -
- - - -

{{__('Search results')}}

- - @if (!isset($ipInfo->message)) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{__('Origin country')}}{{$ipInfo->country_name ?? 'N/A'}}
{{__('State/Province')}}{{$ipInfo->state_prov ?? 'None'}}
{{__('District (if any)')}}{{$ipInfo->district ?? 'N/A'}}
{{__('City')}}{{$ipInfo->city ?? 'N/A'}}
{{__('Postal code')}}{{$ipInfo->zipcode ?? 'N/A'}}
{{__('Geographical coordinates')}}{{$ipInfo->latitude ?? 0}}, {{$ipInfo->longitude ?? 0}}
{{__('European?')}}{{($ipInfo->is_eu) ? __('Yes') : __('No')}}
{{__('ISP')}}{{$ipInfo->isp ?? 'N/A'}}
{{__('Organization')}}{{$ipInfo->organization ?? 'N/A'}}
{{__('Connection type (e.g. datacenter, home)')}}{{$ipInfo->connection_type ?? 'N/A'}}
{{__('Timezone')}}{{$ipInfo->time_zone->name ?? __('N/A')}}
- - @else -
- - {{__("This query didn't return any results.")}} -

- {{$ipInfo->message}} -

- -
- @endif - - -
- - - - @if($demoActive) -
-

{{ __('This feature is disabled') }}

-
- @endif - -
- @csrf - @method('PATCH') - - - - - - - - - -

- {{__('messages.profile.edituser_consequence')}} -

- -
- - - - - - @foreach($roles as $roleName => $status) - - - - - - @endforeach - - - - -
{{ ucfirst($roleName) }}
- -
- -
- - - - - - - -
- - @endif - - -
@@ -308,7 +58,7 @@
- @if ($profile->user->isBanned()) + @if (is_array($suspensionInfo))

{{$profile->user->name}}

@else

{{$profile->user->name}}

@@ -316,72 +66,16 @@

{{$profile->profileShortBio}}

{{__('Member since :date', ['date' => $since])}}

- @if (Auth::user()->hasRole('admin')) - - @endif @if ($profile->user->is(Auth::user())) - @elseif (Auth::user()->hasRole('admin') && $profile->user->isStaffMember()) - @endif
- - -
- @if (Auth::user()->hasRole('admin')) - -
- -
-
- -
-
-
- -
- - @if (!$profile->user->isBanned()) -
- @else -
- - @method('DELETE') - @csrf - - -
- @endif -
- -
-
- -
-
-
- - -
- @endif -
diff --git a/resources/views/dashboard/user/profile/useraccount.blade.php b/resources/views/dashboard/user/profile/useraccount.blade.php index bf21354..62423c6 100755 --- a/resources/views/dashboard/user/profile/useraccount.blade.php +++ b/resources/views/dashboard/user/profile/useraccount.blade.php @@ -33,7 +33,7 @@

{{ __('Deleting your account is an irreversible process. The following data will be deleted (including personally identifiable data):') }}

  • {{ __('Last IP address') }}
  • -
  • {{ __('Name, Email and MC Username') }}
  • +
  • {{ __('Your name and email address') }}
  • {{ __('Your previous applications') }}
  • {{ __('Your profile data and preferences') }}
  • {{ __('Any other information stored in your user profile') }}
  • @@ -60,21 +60,14 @@ @csrf @method('PATCH') -
    - - -

    {{ __('For your security, your password is always required for sensitive operations.') }} {{ __('Forgot your password?') }}

    -
    + + {{ __('For your security, your password is always required for sensitive operations.') }} + - @if (Auth::user()->has2FA()) -
    + + {{ __('You cannot recover lost 2FA secrets.') }} + - - -

    {{ __('You cannot recover lost 2FA secrets.') }}

    - -
    - @endif @@ -247,6 +240,9 @@ + @@ -270,30 +266,102 @@
@endif -
{{__('Change Password')}}
-

{{__('Change your password here. This will log you out from all existing sessions for your security.')}}

+ @if (!Auth::user()->hasPassword()) -
+
{{ __('Your :appName account does not have a password', ['appName' => config('app.name')]) }}
- @csrf - @method('PATCH') - - -

{!! __('Forgot password? Reset it here!') !!}

+

{!! __('Because your account is linked to one or more OAuth providers (such as Discord), there is no need for your account to use a password.', ['oAuthExplanationLink' => 'https://en.wikipedia.org/wiki/OAuth']) !!}

+

{{ __('Because several sensitive actions on the app require you to confirm your password, you may define one below. This will also allow you to unlink your accounts and login with a password.') }}

-
+ + @csrf + @method('PATCH') - - +

{{ __('Setting a new password:') }}

- - +
+
+ + +
+
+ + +
+
+ + + + +

{{ __('You will be logged out afterwards.') }}

+ + @else + +
{{__('Change Password')}}
+

{{__('Change your password here. This will log you out from all existing sessions for your security.')}}

+ +
+ + @csrf + @method('PATCH') + + +

{!! __('Forgot password? Reset it here!') !!}

+ +
+ + + + + + + +
+ +
+ + + @endif +
+
+ +
{{ __('Connected Accounts') }}
+

{{__('Manage your connected external accounts here.')}}

+ + @if(Auth::user()->hasDiscordConnection()) +

{{ __('Your account is currently connected to Discord.') }}

+ @if(!Auth::user()->hasPassword()) + + {{ __("We've disabled unlinking your Discord account for now. To re-enable it, please add a password in the Account Security screen. This is to ensure you still have access to your account even after you change your Discord account email address.") }} + + @else + +

{{ __("If you choose to disconnect your Discord account, we'll let Discord know by revoking your authorization. This means you will no longer be able to apply for positions that require a linked Discord account, and you may also lose any server privileges/roles that may have been granted through your linked account.") }}

+

{{ __(" You'll be able to link your account again through this page or by signing in with Discord again, but remember that if you changed your Discord email address, you will no longer have access to your old account.") }}

+
+ @endif +
+
+ {{ __('Connected to Discord') }} +
+
+ @if(Auth::user()->hasPassword()) +
+ @method('PATCH') + @csrf + {{ __('Disconnect') }} +
+ @else + {{ __('Disconnect') }} + @endif +
+ @else +

{{ __('Your Discord account is not connected. Connect your Discord account below in order to apply for positions that require it.') }}

+ {{ __('Connect to Discord') }} + @endif - - -
{{__('Two Factor Authentication')}}
diff --git a/resources/views/dashboard/user/profile/userprofile.blade.php b/resources/views/dashboard/user/profile/userprofile.blade.php index c2f1264..58b9200 100755 --- a/resources/views/dashboard/user/profile/userprofile.blade.php +++ b/resources/views/dashboard/user/profile/userprofile.blade.php @@ -35,184 +35,247 @@ @section('content') -
+ @if (!is_null($profile)) -
+ -
+

{{ __('Deleting your profile is an irreversible operation. You will not be able to recover any previously entered information.') }}

+

{{ __('After you delete your profile, the following will happen:') }}

-
+
    +
  • {{ __('Your account will no longer be listed on the public Directory page.') }}
  • +
  • {{ __('Your profile page will become inaccesible to everyone with an error message.') }}
  • +
  • {{ __('Your profile configuration page will become inaccessible with an error message.') }}
  • +
  • {{ __('In a future update, the public Directory page might become inaccessible to users without a profile.') }}
  • +
-
-
-
- @if($profile->avatarPreference == 'gravatar') - {{ __('User profile picture') }} - @else - {{ __('User profile picture') }} - @endif +

{{ __('If you change your mind and want your profile back, you will be able to create a new blank profile from your profile configuration page, restoring access to the features mentioned above.') }}

+

{{ __('Are you sure you want to delete your profile?') }}

+ + +
+ @csrf + @method('DELETE') + {{ __('Yes, delete my profile') }} +
+
+ + + +
+ +
+ +
+ +
+ +
+
+
+ @if($profile->avatarPreference == 'gravatar') + {{ __('User profile picture') }} + @else + {{ __('User profile picture') }} + @endif +
+ +

{{Auth::user()->name}}

+ +

{{$profile->profileShortBio}}

+ +
+ + + +
-

{{Auth::user()->name}}

+ - - - - - -
+
+ -
-
+
+ @method('PATCH') + @csrf -
+
-
+
+
- +
- @method('PATCH') - @csrf - -
- -
- -
- -
- -

{{__('Basic Information')}}

- -
- -
- -
- -
- - - - -
- -
- -
- - - - -
- -
- - - -

{{__('Markdown supported')}}

- -
- -
- -
- -
- -
- -
- -
-

{{__('Preferences & Media')}}

-
- -
- - - -
- - - - +

{{__('Basic Information')}}

- +
-
-
- -
- -
+
-
-
- -
- -
+
-
-
- -
- -
+ + + +
-
-
-
- + +
+ + + + +
+ +
+ + + + +
+ +
+ + + +

{{__('Markdown supported')}}

+ +
+
-
+
-
+
-
+
+

{{__('Preferences & Media')}}

+
-
+
- + -
+
-
+ - + + +
+ + + +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + + + @else + +
+ +
+ + + + + +

{{ __("Your account currently has no profile on file. This means that your account will not be visibile in the public profile Directory, and you will not be able to access your profile until you create one.") }}

+

{{ __("There are many benefits to creating a profile! For instance, you'll be able to set a profile picture, as well as sharing your social media and anything else you'd like. Creating a profile is instant and you can begin configuring it right away. If you later change your mind, you may also delete your profile at any time.") }}

+
+ + +
+ @csrf + {{ __('Create profile') }} +
+
+
+
+ +
+ + @endif @stop @section('footer') diff --git a/resources/views/dashboard/user/viewapp.blade.php b/resources/views/dashboard/user/viewapp.blade.php index 609b4db..58db0ef 100755 --- a/resources/views/dashboard/user/viewapp.blade.php +++ b/resources/views/dashboard/user/viewapp.blade.php @@ -65,7 +65,7 @@
@csrf @method('PATCH') - +
@@ -156,8 +156,13 @@

{{__('Applicant Name')}} {{$application->user->name}}

+ + @if ($application->user->hasDiscordConnection()) +

{{ __('Discord tag: ') }} {{ __(':discordUsername (Connected account)', ['discordUsername' => $application->user->username]) }}

+ @endif + @if (Auth::user()->hasRole('hiringManager')) -

{{__('Applicant IP Address')}} {{ (!$shouldCollect) ? __('0.0.0.0 (censored)') : $application->user->originalIP }}

+

{{__('Applicant IP Address')}} {{ (!$shouldCollect) ? __('0.0.0.0 (censored)') : $application->user->registrationIp }}

@endif

{{__('Application Date')}} {{$application->created_at}}

{{__('Last updated')}}{{$application->updated_at}}

@@ -222,7 +227,7 @@
- +
diff --git a/resources/views/errors/401.blade.php b/resources/views/errors/401.blade.php old mode 100644 new mode 100755 diff --git a/resources/views/errors/403.blade.php b/resources/views/errors/403.blade.php old mode 100644 new mode 100755 diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php old mode 100644 new mode 100755 diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php old mode 100644 new mode 100755 diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php old mode 100644 new mode 100755 diff --git a/resources/views/home.blade.php b/resources/views/home.blade.php index 05093d9..e5e2643 100755 --- a/resources/views/home.blade.php +++ b/resources/views/home.blade.php @@ -9,7 +9,7 @@ @foreach($positions as $position) - + @if (is_null($position->vacancyFullDescription)) @@ -25,7 +25,7 @@ {!! $position->vacancyFullDescription !!}

- {{__('Last updated @ :lastUpdatedTimeValue', [':lastUpdatedTimeValue' => $position->updated_at])}} + {{__('Last updated :lastUpdatedRelativeTimeValue', ['lastUpdatedRelativeTimeValue' => $position->updated_at->diffForHumans()])}}

@endif @@ -43,28 +43,6 @@
-
- -
-
- - - - -
- -
- - @if ($demoActive)
@@ -78,42 +56,10 @@
@endif -
- -
- -
- -

Junte-se ao nosso Discord!

-

O Discord é o coração da nossa comunidade — onde toda a diversão acontece.

-
-

Venha conhecer a nossa comunidade de perto aderindo ao nosso servidor Discord. Conheça novos amigos, jogos, vários minigames divertidos e fale com a nossa equipe amigável! Todo mundo é bem-vindo.

-

Registre-se também no nosso portal online, que lhe permite gerir suas candidaturas à nossa equipe, ligar a sua conta Discord (e receber vantagens no servidor!), ver o diretório de usuários, fazer doação e muito mais!

-

- - - - - Aderir ao Discord - Aderir ao portal -

- -
- -
- -
- -
- -
-
- community decorative svg -

Confira os cargos disponíveis

Quer colaborar com a equipe da Games Club? Estamos recrutando! Confira um dos nossos cargos abertos. Uma boa equipe é um pilar de uma comunidade bem-sucedida.

@@ -148,18 +94,33 @@
@@ -207,118 +168,13 @@
-

Convencido?

+

{{ __('A gestão da :appName responde a todas candidaturas dentro de 48 horas.', ['appName' => config('app.name')]) }}

+

{!! __('Se você tiver algum dúvida sobre a sua conta de recrutamento, candidatura, ou qualquer outra questão, visite o nosso site de atendimento, ou envie-nos um email.', ['supportURL' => config('app.support_url'), 'supportEmail' => config('app.support_email')]) !!}

-
- -
-

- Esperamos você! Junte-se hoje e desfrute de uma nova experiência. -

-
- - -
- -
- -
- -

{{__('Any questions? Leave a message!')}}

-

{{__('*This is not an application form. Any applications sent here will be ignored. Additionally, please keep messages on topic (about this site only). For anything else, please use other contact options available.')}}

- - -
- -
- -
- -
- - - -
- @csrf - - - - -
- -
-
- - - - -
-
- -
- -
- - - - -
- -
- -
- - -
- -
- - - - -
- -
- -
- -
- - - -
- -
- -
- -
- -
- -
- -
- - - - -
- - - -
- -
diff --git a/resources/views/mail/account-locked.blade.php b/resources/views/mail/account-locked.blade.php old mode 100644 new mode 100755 diff --git a/resources/views/mail/adminreset.blade.php b/resources/views/mail/adminreset.blade.php new file mode 100755 index 0000000..786ea9f --- /dev/null +++ b/resources/views/mail/adminreset.blade.php @@ -0,0 +1,22 @@ +@component('mail::message') +# Hi {{ $name }}, + +Important notification about your {{ config('app.name') }} account: + +This email serves to inform you that our administration team has forcefully invalidated your account's password. This means that you will no longer be able to sign in using your old credentials. + +You will need to [reset your password]({{ route('password.email') }}) to set a new password if you want to keep using your account. Admins forcefully reset account passwords for a variety of reasons, including, but not limited to: + + - Suspected compromised password; + - [Your password appeared in a data breach](https://haveibeenpwned.com/Passwords); + - A technical issue with your account; + - Our service suffered a security breach and a system-wide reset was initiated; + - and finally, forced reset at your request. + +We may or may not inform you of the specific cause for your forced reset. + +Please note that we take account security seriously, and forced password resets are just one of many security measures we employ to keep your account and data secure. If you have any questions, please do not hesitate in contacting us. + +Thank you,
+The team at {{ config('app.name') }} +@endcomponent diff --git a/resources/views/mail/two-factor-reset.blade.php b/resources/views/mail/two-factor-reset.blade.php new file mode 100644 index 0000000..c9219ec --- /dev/null +++ b/resources/views/mail/two-factor-reset.blade.php @@ -0,0 +1,12 @@ +@component('mail::message') +# Hi {{ $name }}, + +Important security notification regarding your account at {{ config('app.name') }}: + +Your account was previously secured with two-factor authentication. This is no longer the case. An administrator has disabled two-factor authentication for your account. Admins only reset two-factor authentication after an identity verification is complete. + +As a result of this action and as an additional security measure, your password has also been voided, which means you'll need to [reset it]({{ route('password.email') }}) if you want to keep using the app. + +Thank you,
+The team at {{ config('app.name') }} +@endcomponent diff --git a/routes/web.php b/routes/web.php index 1168f6a..29e3b40 100755 --- a/routes/web.php +++ b/routes/web.php @@ -22,6 +22,7 @@ use App\Http\Controllers\AbsenceController; use App\Http\Controllers\ApplicationController; use App\Http\Controllers\AppointmentController; +use App\Http\Controllers\Auth\DiscordController; use App\Http\Controllers\Auth\LoginController; use App\Http\Controllers\Auth\TwofaController; use App\Http\Controllers\BanController; @@ -78,10 +79,10 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('verify2FA'); - Route::get('/auth/redirect/discord', [LoginController::class, 'discordRedirect']) + Route::get('/redirect/discord', [DiscordController::class, 'discordRedirect']) ->name('discordRedirect'); - Route::get('/auth/callback/discord', [LoginController::class, 'discordCallback']) + Route::get('/callback/discord', [DiscordController::class, 'discordCallback']) ->name('discordCallback'); }); @@ -97,6 +98,9 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo Route::get('/accounts/{accountID}/dg/process-delete/{action}', [UserController::class, 'processDeleteConfirmation']) ->name('processDeleteConfirmation'); + Route::get('/apply/discord/{vacancySlug}', [ApplicationController::class, 'discordApply']) + ->name('discord-apply'); + Route::group(['middleware' => ['auth', 'forcelogout', 'passwordexpiration', '2fa', 'verified']], function () { @@ -107,9 +111,6 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('dashboard') ->middleware('eligibility'); - Route::get('users/directory', [ProfileController::class, 'index']) - ->name('directory'); - Route::resource('teams', TeamController::class); Route::post('teams/{team}/invites/send', [TeamController::class, 'invite']) @@ -192,6 +193,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo // Further locking down the profile section by adding the middleware to everything but the required routes Route::group(['prefix' => '/profile'], function () { + Route::get('/settings', [ProfileController::class, 'showProfile']) ->name('showProfileSettings') ->middleware('passwordredirect'); @@ -204,6 +206,13 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('showSingleProfile') ->middleware('passwordredirect'); + Route::post('user/profile', [ProfileController::class, 'createProfile']) + ->name('createProfile') + ->middleware('passwordredirect'); + + Route::delete('user/profile', [ProfileController::class, 'deleteProfile']) + ->name('deleteProfile') + ->middleware('passwordredirect'); Route::get('/settings/account', [UserController::class, 'showAccount']) @@ -214,6 +223,14 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('changePassword'); + Route::patch('/settings/account/add-password', [UserController::class, 'setPassword']) + ->name('addPassword'); + Route::patch('/settings/account/unlink-oauth', [UserController::class, 'unlinkDiscordAccount']) + ->name('unlink-discord-account'); + Route::patch('settings/account/add-age', [UserController::class, 'addDob']) + ->name('add-dob'); + + Route::patch('/settings/account/change-email', [UserController::class, 'changeEmail']) ->name('changeEmail') @@ -283,18 +300,28 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo Route::patch('settings/game/update', [OptionsController::class, 'saveGameIntegration']) ->name('saveGameIntegration'); - Route::post('players/ban/{user}', [BanController::class, 'insert']) + + Route::post('accounts/suspend/{user}', [UserController::class, 'suspend']) ->name('banUser'); - Route::delete('players/unban/{user}', [BanController::class, 'delete']) + Route::delete('accounts/unsuspend/{user}', [UserController::class, 'unsuspend']) ->name('unbanUser'); - Route::delete('players/delete/{user}', [UserController::class, 'delete']) + Route::delete('accounts/delete/{user}', [UserController::class, 'delete']) ->name('deleteUser'); - Route::patch('players/update/{user}', [UserController::class, 'update']) + Route::patch('accounts/force-reset/{user}', [UserController::class, 'forcePasswordReset']) + ->name('force-reset-user'); + + Route::patch('accounts/reset-twofa/{user}', [UserController::class, 'reset2FASecret']) + ->name('reset-twofa'); + + Route::patch('accounts/update/{user}', [UserController::class, 'update']) ->name('updateUser'); + Route::get('accounts/manage/{user}', [UserController::class, 'showAcocuntManagement']) + ->name('manageUser'); + Route::get('positions', [VacancyController::class, 'index']) ->name('showPositions'); @@ -335,7 +362,7 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('updateForm'); - Route::group(['prefix' => 'devtools'], function () { + Route::group(['prefix' => 'developers'], function () { Route::get('/', [DevToolsController::class, 'index']) ->name('devTools'); @@ -351,7 +378,10 @@ Route::group(['prefix' => LaravelLocalization::setLocale(), 'middleware' => ['lo ->name('devForceEvaluateVotes'); Route::delete('/suspensions/purge-expired', [DevToolsController::class, 'purgeSuspensions']) - ->name('devPurgeExpired'); + ->name('devPurgeExpiredSuspensions'); + + Route::delete('/absences/purge-expired', [DevToolsController::class, 'endAbsencesNow']) + ->name('devPurgeExpiredAbsences'); });