Beta version

This commit is too large to list all changes.
This commit is contained in:
Miguel Nogueira 2020-06-27 00:32:33 +01:00
parent d15c0cb12f
commit 5a8c080a31
135 changed files with 8534 additions and 12774 deletions

View File

@ -18,6 +18,9 @@ RECAPTCHA_PRIVATE_KEY=
RECAPTCHA_VERIFY_URL="https://www.google.com/recaptcha/api/siteverify"
# WARNING: Your contact form will be useless if you change this value. Only change this URL if Google updates it.
IPGEO_API_KEY=""
IPGEO_API_URL=""
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync

View File

@ -94,10 +94,12 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/sebastian/version" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sentry/sentry" />
<excludeFolder url="file://$MODULE_DIR$/vendor/sentry/sentry-laravel" />
<excludeFolder url="file://$MODULE_DIR$/vendor/spatie/laravel-permission" />
<excludeFolder url="file://$MODULE_DIR$/vendor/swiftmailer/swiftmailer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/console" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/css-selector" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/debug" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/deprecation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/error-handler" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/event-dispatcher-contracts" />
@ -108,14 +110,18 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/options-resolver" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-ctype" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-iconv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-idn" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-mbstring" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php72" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php73" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-php80" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/polyfill-uuid" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/process" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/routing" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/service-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/string" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/translation-contracts" />
<excludeFolder url="file://$MODULE_DIR$/vendor/symfony/var-dumper" />

View File

@ -121,6 +121,12 @@
<path value="$PROJECT_DIR$/vendor/clue/stream-filter" />
<path value="$PROJECT_DIR$/vendor/jean85/pretty-package-versions" />
<path value="$PROJECT_DIR$/vendor/http-interop/http-factory-guzzle" />
<path value="$PROJECT_DIR$/vendor/spatie/laravel-permission" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-normalizer" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/symfony/string" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-intl-grapheme" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-php80" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.2" />

22
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for XDebug",
"type": "php",
"request": "launch",
"port": 9000
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 9000
}
]
}

View File

@ -15,6 +15,8 @@ class Application extends Model
];
public function user()
{
return $this->belongsTo('App\User', 'applicantUserID', 'id');
@ -35,6 +37,12 @@ class Application extends Model
return $this->belongsToMany('App\Vote', 'votes_has_application');
}
public function comments()
{
return $this->hasMany('App\Comment', 'applicationID', 'id');
}
public function setStatus($status)
{
return $this->update([

View File

@ -16,6 +16,7 @@ class Appointment extends Model
public function application()
{
// FIXME: Possible bug here, where laravel looks for the wrong column in the applications table.
return $this->belongsTo('App\Application', 'id', 'applicationID');
}

25
app/Ban.php Normal file
View File

@ -0,0 +1,25 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Ban extends Model
{
public $fillable = [
'userID',
'reason',
'bannedUntil',
'userAgent',
'authorUserID'
];
public function user()
{
return $this->belongsTo('App\User', 'userID', 'id');
}
}

26
app/Comment.php Normal file
View File

@ -0,0 +1,26 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
protected $fillable = [
'authorID',
'applicationID',
'text'
];
public function application()
{
return $this->belongsTo('App\Application', 'applicationID', 'id');
}
public function user()
{
return $this->belongsTo('App\User', 'authorID', 'id');
}
}

View File

@ -96,6 +96,9 @@ class CountVotes extends Command
$this->info('✓ Dispatched promotion event for applicant ' . $application->user->name);
if (!$this->option('dryrun'))
{
$application->response->vacancy->vacancyCount -= 1;
$application->response->vacancy->save();
event(new ApplicationApprovedEvent(Application::find($application->id)));
}
else

View File

@ -4,6 +4,7 @@ namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use App\Jobs\CleanBans;
class Kernel extends ConsoleKernel
{
@ -27,7 +28,11 @@ class Kernel extends ConsoleKernel
// $schedule->command('inspire')->hourly();
$schedule->command('vote:evaluate')
->everyFiveMinutes();
->daily();
// Production value: Every day
$schedule->job(new CleanBans)
->daily();
// Production value: Every day
}

42
app/CustomFacades/IP.php Normal file
View File

@ -0,0 +1,42 @@
<?php
namespace App\CustomFacades;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class IP
{
/**
* Looks up information on a specified IP address. Caches results automatically.
* @param string $IP IP address to lookup
* @return object
*/
public function lookup(string $IP): object
{
if (empty($IP))
{
throw new LogicException(__METHOD__ . 'is missing parameter IP!');
}
$params = [
'apiKey' => config('general.keys.ipapi.apikey'),
'ip' => $IP
];
// TODO: Maybe unwrap this? Methods are chained here
return json_decode(Cache::remember($IP, 3600, function() use ($IP)
{
return Http::get(config('general.urls.ipapi.ipcheck'), [
'apiKey' => config('general.keys.ipapi.apikey'),
'ip' => $IP
])->body();
}));
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class NewApplicationEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use App\User;
use App\Ban;
class UserBannedEvent
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $user;
public $ban;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(User $user, Ban $ban)
{
$this->user = $user;
$this->ban = $ban;
}
}

13
app/Facades/IP.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class IP extends Facade
{
protected static function getFacadeAccessor()
{
return 'ipInformationFacade';
}
}

13
app/Facades/UUID.php Normal file
View File

@ -0,0 +1,13 @@
<?php
namespace App\Facades;
use Illuminate\Support\Facades\Facade;
class UUID extends Facade
{
protected static function getFacadeAccessor()
{
return 'uuidConversionFacade';
}
}

View File

@ -3,13 +3,20 @@
namespace App\Http\Controllers;
use App\Application;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use App\Response;
use App\Vacancy;
use App\User;
use App\Events\ApplicationDeniedEvent;
use App\Notifications\NewApplicant;
use App\Notifications\ApplicationMoved;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
class ApplicationController extends Controller
{
@ -21,15 +28,15 @@ class ApplicationController extends Controller
{
if ($vote->userID == Auth::user()->id)
{
Log::debug('Match');
$allvotes->push($vote);
}
}
return $allvotes->count() == 1;
return ($allvotes->count() == 1) ? false : true;
}
public function showUserApps()
{
@ -37,16 +44,22 @@ class ApplicationController extends Controller
->with('applications', Auth::user()->applications);
}
public function showUserApp(Request $request, $applicationID)
{
$application = Application::find($applicationID);
$this->authorize('view', $application);
if (!is_null($application))
{
return view('dashboard.user.viewapp')
->with(
[
'application' => $application,
'comments' => $application->comments,
'structuredResponses' => json_decode($application->response->responseData, true),
'formStructure' => $application->response->form,
'vacancy' => $application->response->vacancy,
@ -63,6 +76,8 @@ class ApplicationController extends Controller
}
public function showAllPendingApps()
{
return view('dashboard.appmanagement.outstandingapps')
@ -70,6 +85,9 @@ class ApplicationController extends Controller
}
public function showPendingInterview()
{
$applications = Application::with('appointment', 'user')->get();
@ -109,6 +127,8 @@ class ApplicationController extends Controller
]);
}
public function showPeerReview()
{
return view('dashboard.appmanagement.peerreview')
@ -116,11 +136,16 @@ class ApplicationController extends Controller
}
public function renderApplicationForm(Request $request, $vacancySlug)
{
// FIXME: Get rid of references to first(), this is a wonky query
$vacancyWithForm = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get();
if (!$vacancyWithForm->isEmpty())
$firstVacancy = $vacancyWithForm->first();
if (!$vacancyWithForm->isEmpty() && $firstVacancy->vacancyCount !== 0 && $firstVacancy->vacancyStatus == 'OPEN')
{
return view('dashboard.application-rendering.apply')
@ -133,15 +158,25 @@ class ApplicationController extends Controller
}
else
{
abort(404, 'We\'re ssssorry, but the application form you\'re looking for could not be found.');
abort(404, 'The application you\'re looking for could not be found or it is currently unavailable.');
}
}
public function saveApplicationAnswers(Request $request, $vacancySlug)
{
$vacancy = Vacancy::with('forms')->where('vacancySlug', $vacancySlug)->get();
if ($vacancy->first()->vacancyCount == 0 || $vacancy->first()->vacancyStatus !== 'OPEN')
{
$request->session()->flash('error', 'This application is unavailable.');
return redirect()->back();
}
Log::info('Processing new application!');
$formStructure = json_decode($vacancy->first()->forms->formStructure, true);
@ -179,7 +214,7 @@ class ApplicationController extends Controller
Log::info('Registered form response for user ' . Auth::user()->name . ' for vacancy ' . $vacancy->first()->vacancyName);
Application::create([
$application = Application::create([
'applicantUserID' => Auth::user()->id,
'applicantFormResponseID' => $response->id,
'applicationStatus' => 'STAGE_SUBMITTED',
@ -187,6 +222,14 @@ class ApplicationController extends Controller
Log::info('Submitted application for user ' . Auth::user()->name . ' with response ID' . $response->id);
foreach(User::all() as $user)
{
if ($user->hasRole('admin'))
{
$user->notify((new NewApplicant($application, $vacancy->first()))->delay(now()->addSeconds(10)));
}
}
$request->session()->flash('success', 'Thank you for your application! It will be reviewed as soon as possible.');
return redirect()->to(route('showUserApps'));
}
@ -210,15 +253,15 @@ class ApplicationController extends Controller
{
case 'deny':
Log::info('User ' . Auth::user()->name . ' has denied application ID ' . $application->id);
$request->session()->flash('success', 'Application denied.');
$application->setStatus('DENIED');
event(new ApplicationDeniedEvent($application));
break;
case 'interview':
Log::info('User ' . Auth::user()->name . ' has moved application ID ' . $application->id . 'to interview stage');
$request->session()->flash('success', 'Application moved to interview stage! (:');
$application->setStatus('STAGE_INTERVIEW');
$application->user->notify(new ApplicationMoved());
break;
default:

View File

@ -7,6 +7,8 @@ use App\Http\Requests\SaveNotesRequest;
use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Appointment;
use App\Notifications\ApplicationMoved;
use App\Notifications\AppointmentScheduled;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
@ -48,6 +50,7 @@ class AppointmentController extends Controller
'scheduled' => now()
]);
$app->user->notify(new AppointmentScheduled($appointment));
$request->session()->flash('success', 'Appointment successfully scheduled @ ' . $appointmentDate->toDateTimeString());
}
@ -70,10 +73,12 @@ class AppointmentController extends Controller
if (!is_null($application))
{
// NOTE: This is a little confusing, refactor
$application->appointment->appointmentStatus = (in_array($status, $validStatuses)) ? strtoupper($status) : 'SCHEDULED';
$application->appointment->save();
$application->setStatus('STAGE_PEERAPPROVAL');
$application->user->notify(new ApplicationMoved());
$request->session()->flash('success', 'Interview finished! Staff members can now vote on it.');
}

View File

@ -2,9 +2,11 @@
namespace App\Http\Controllers\Auth;
use App\User;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
@ -19,7 +21,9 @@ class LoginController extends Controller
|
*/
use AuthenticatesUsers;
use AuthenticatesUsers {
attemptLogin as protected originalAttemptLogin;
}
/**
* Where to redirect users after login.
@ -37,4 +41,29 @@ class LoginController extends Controller
{
$this->middleware('guest')->except('logout');
}
// We can't customise the error message, since that would imply overriding the login method, which is large.
// Also, the user should never know that they're banned.
public function attemptLogin(Request $request)
{
$user = User::where('email', $request->email)->first();
if ($user)
{
$isBanned = $user->isBanned();
if ($isBanned)
{
return false;
}
else
{
return $this->originalAttemptLogin($request);
}
}
return $this->originalAttemptLogin($request);
}
}

View File

@ -43,6 +43,21 @@ 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.
*
@ -78,15 +93,10 @@ class RegisterController extends Controller
'originalIP' => request()->ip()
]);
Profile::create([
'profileShortBio' => 'Write a one-liner about you here!',
'profileAboutMe' => 'Tell us a bit about you.',
'socialLinks' => '{}',
'userID' => $user->id
]);
// It's not the registration controller's concern to create a profile for the user,
// so this code has been moved to it's respective observer, following the separation of concerns pattern.
$user->assignRole('user');
return $user;
}
}

View File

@ -0,0 +1,91 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Ban;
use App\User;
use App\Events\UserBannedEvent;
use App\Http\Requests\BanUserRequest;
class BanController extends Controller
{
public function insert(BanUserRequest $request, User $user)
{
if ($user->is(Auth::user()))
{
$request->session()->flash('error', 'You can\'t ban yourself!');
return redirect()->back();
}
if (is_null($user->bans))
{
$reason = $request->reason;
$duration = strtolower($request->durationOperator);
$durationOperand = $request->durationOperand;
if (!empty($duration))
{
$expiryDate = now();
switch($duration)
{
case 'days':
$expiryDate->addDays($duration);
break;
case 'weeks':
$expiryDate->addWeeks($duration);
break;
case 'months':
$expiryDate->addMonths($duration);
break;
case 'years':
$expiryDate->addYears($duration);
break;
}
}
$ban = Ban::create([
'userID' => $user->id,
'reason' => $request->reason,
'bannedUntil' => $expiryDate->toDateTimeString() ?? null,
'userAgent' => "Unknown",
'authorUserID' => Auth::user()->id
]);
event(new UserBannedEvent($user, $ban));
$request->session()->flash('success', 'User banned successfully! Ban ID: #' . $ban->id);
}
else
{
$request->session()->flash('error', 'User already banned!');
}
return redirect()->back();
}
public function delete(Request $request, User $user)
{
if (!is_null($user->bans))
{
$user->bans->delete();
$request->session()->flash('success', 'User unbanned successfully!');
}
else
{
$request->session()->flash('error', 'This user isn\'t banned!');
}
return redirect()->back();
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\NewCommentRequest;
use App\Comment;
use App\Application;
use App\Notifications\NewComment;
use App\User;
class CommentController extends Controller
{
public function index()
{
//
}
public function insert(NewCommentRequest $request, Application $application)
{
// Type hinting makes laravel automatically validate everything
$comment = Comment::create([
'authorID' => Auth::user()->id,
'applicationID' => $application->id,
'text' => $request->comment
]);
if ($comment)
{
foreach (User::all() as $user)
{
if ($user->isStaffMember())
{
$user->notify(new NewComment($comment, $application));
}
}
$request->session()->flash('success', 'Comment posted! (:');
}
else
{
$request->session()->flash('error', 'Something went wrong while posting your comment!');
}
return redirect()->back();
}
public function delete(Request $request, Comment $comment)
{
if (Auth::user()->is($comment->user) || Auth::user()->hasRole('admin'))
{
$comment->delete();
$request->session()->flash('success', 'Comment deleted!');
}
$request->session()->flash('error', 'You do not have permission to delete this comment!');
return redirect()->back();
}
}

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Vacancy;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class HomeController extends Controller
{
@ -17,7 +18,13 @@ class HomeController extends Controller
// TODO: Relationships for Applications, Users and Responses
// Also prevent apps if user already has one in the space of 30d
// Display apps in the relevant menus
$positions = DB::table('vacancies')
->where('vacancyStatus', 'OPEN')
->where('vacancyCount', '!=', 0)
->get();
return view('home')
->with('positions', Vacancy::where('vacancyStatus', 'OPEN')->get());
->with('positions', $positions);
}
}

View File

@ -5,15 +5,20 @@ namespace App\Http\Controllers;
use App\Http\Requests\ProfileSave;
use Illuminate\Support\Facades\Log;
use App\Profile;
use App\User;
use App\Facades\IP;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Spatie\Permission\Models\Role;
class ProfileController extends Controller
{
public function showProfile()
{
$socialMediaProfiles = json_decode(Auth::user()->profile->socialLinks, true);
$socialLinks = Auth::user()->profile->socialLinks ?? "[]";
$socialMediaProfiles = json_decode($socialLinks, true);
return view('dashboard.user.profile.userprofile')
->with([
@ -26,11 +31,56 @@ class ProfileController extends Controller
}
// Route model binding
public function showSingleProfile(Request $request, User $user)
{
$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;
}
}
if (Auth::user()->is($user) || Auth::user()->can('profiles.view.others'))
{
return view('dashboard.user.profile.displayprofile')
->with([
'profile' => $user->profile,
'github' => $socialMediaProfiles['links']['github'] ?? 'UpdateMe',
'twitter' => $socialMediaProfiles['links']['twitter'] ?? 'UpdateMe',
'insta' => $socialMediaProfiles['links']['insta'] ?? 'UpdateMe',
'discord' => $socialMediaProfiles['links']['discord'] ?? 'UpdateMe#12345',
'since' => $createdDate->englishMonth . " " . $createdDate->year,
'ipInfo' => IP::lookup($user->originalIP),
'roles' => $roleList
]);
}
else
{
abort(403, 'You cannot view someone else\'s profile.');
}
}
public function saveProfile(ProfileSave $request)
{
// TODO: Implement profile security policy for logged in users
$profile = Profile::find(Auth::user()->id);
// TODO: Switch to route model binding
$profile = User::find(Auth::user()->id)->profile;
$social = [];
if (!is_null($profile))
@ -57,7 +107,7 @@ class ProfileController extends Controller
$profile->avatarPreference = $avatarPref;
$profile->socialLinks = json_encode($social);
$profile->save();
$newProfile = $profile->save();
$request->session()->flash('success', 'Profile settings saved successfully.');

View File

@ -5,23 +5,117 @@ namespace App\Http\Controllers;
use App\Http\Requests\ChangeEmailRequest;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\FlushSessionsRequest;
use App\Http\Requests\DeleteUserRequest;
use App\Http\Requests\SearchPlayerRequest;
use App\Http\Requests\UpdateUserRequest;
use App\User;
use App\Ban;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;
use App\Facades\UUID;
use App\Notifications\EmailChanged;
use App\Notifications\ChangedPassword;
use Spatie\Permission\Models\Role;
class UserController extends Controller
{
public function showStaffMembers()
{
return view('dashboard.administration.staff-members');
$staffRoles = [
'reviewer',
'hiringManager',
'admin'
]; // TODO: Un-hardcode this, move to config/roles.php
if (Auth::user()->can('admin.stafflist'))
{
$users = User::with('roles')->get();
$staffMembers = collect([]);
foreach($users as $user)
{
if (empty($user->roles))
{
Log::debug($user->role->name);
Log::debug('Staff list: User without role detected; Ignoring');
continue;
}
foreach($user->roles as $role)
{
if (in_array($role->name, $staffRoles))
{
$staffMembers->push($user);
continue 2; // Skip directly to the next user instead of comparing more roles for the current user
}
}
}
return view('dashboard.administration.staff-members')
->with([
'users' => $staffMembers
]);
}
abort(403, 'Forbidden');
}
public function showPlayers()
{
return view('dashboard.administration.players');
$users = User::with('roles')->get();
$players = collect([]);
foreach($users as $user)
{
// TODO: Might be problematic if we don't check if the role is user
if (count($user->roles) == 1)
{
$players->push($user);
}
}
if (Auth::user()->can('admin.userlist'))
{
return view('dashboard.administration.players')
->with([
'users' => $players,
'bannedUserCount' => Ban::all()->count()
]);
}
abort(403, 'Forbidden');
}
public function showPlayersLike(SearchPlayerRequest $request)
{
$searchTerm = $request->searchTerm;
$matchingUsers = User::query()
->where('name', 'LIKE', "%{$searchTerm}%")
->orWhere('email', 'LIKE', "%{$searchTerm}%")
->get();
if (!$matchingUsers->isEmpty())
{ $request->session()->flash('success', 'There were ' . $matchingUsers->count() . ' user(s) matching your search.');
return view('dashboard.administration.players')
->with([
'users' => $matchingUsers,
'bannedUserCount' => Ban::all()->count()
]);
}
else
{
$request->session()->flash('error', 'Your search term did not return any results.');
return redirect(route('registeredPlayerList'));
}
}
public function showAccount()
@ -62,9 +156,9 @@ class UserController extends Controller
'userID' => $user->id,
'timestamp' => now()
]);
Auth::logout();
$user->notify(new ChangedPassword());
// After logout, the user gets caught by the auth filter, and it automatically redirects back to the previous page
Auth::logout();
return redirect()->back();
}
@ -84,6 +178,7 @@ class UserController extends Controller
'userID' => $user->id,
'timestamp' => now()
]);
$user->notify(new EmailChanged());
$request->session()->flash('success', 'Your email address has been changed!');
}
@ -95,4 +190,88 @@ class UserController extends Controller
return redirect()->back();
}
public function delete(DeleteUserRequest $request, User $user)
{
if ($request->confirmPrompt == 'DELETE ACCOUNT')
{
$user->delete();
$request->session()->flash('success','User deleted successfully. PII has been erased.');
}
else
{
$request->session()->flash('error', 'Wrong confirmation text! Try again.');
}
return redirect()->route('registeredPlayerList');
}
public function update(UpdateUserRequest $request, User $user)
{
// Mass update would not be possible here without extra code, making route model binding useless
$user->email = $request->email;
$user->name = $request->name;
$user->uuid = $request->uuid;
$existingRoles = Role::all()
->pluck('name')
->all();
$roleDiff = array_diff($existingRoles, $request->roles);
// Adds roles that were selected. Removes roles that aren't selected if the user has them.
foreach($roleDiff as $deselectedRole)
{
if ($user->hasRole($deselectedRole) && $deselectedRole !== 'user')
{
$user->removeRole($deselectedRole);
}
}
foreach($request->roles as $role)
{
if (!$user->hasRole($role))
{
$user->assignRole($role);
}
}
$user->save();
$request->session()->flash('success', 'User updated successfully!');
return redirect()->back();
}
public function terminate(Request $request, User $user)
{
$this->authorize('terminate', Auth::user());
if (!$user->isStaffMember() || $user->is(Auth::user()))
{
$request->session()->flash('error', 'You cannot terminate this user.');
return redirect()->back();
}
foreach ($user->roles as $role)
{
if ($role->name == 'user')
{
continue;
}
$user->removeRole($role->name);
}
Log::info('User ' . $user->name . ' has just been demoted.');
$request->session()->flash('success', 'User terminated successfully.');
//TODO: Dispatch event
return redirect()->back();
}
}

View File

@ -2,9 +2,12 @@
namespace App\Http\Controllers;
use App\Form;
use App\Http\Requests\VacancyRequest;
use App\Vacancy;
use App\User;
use App\Form;
use App\Notifications\VacancyClosed;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
@ -68,6 +71,13 @@ class VacancyController extends Controller
$vacancy->close();
$message = "Position successfully closed!";
foreach(User::all() as $user)
{
if ($user->isStaffMember())
{
$user->notify(new VacancyClosed($vacancy));
}
}
break;
default:

View File

@ -63,6 +63,7 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'eligibility' => \App\Http\Middleware\ApplicationEligibility::class,
'usernameUUID' => \App\Http\Middleware\UsernameUUID::class
'usernameUUID' => \App\Http\Middleware\UsernameUUID::class,
'forcelogout' => \App\Http\Middleware\ForceLogoutMiddleware::class
];
}

View File

@ -0,0 +1,39 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\View;
class Bancheck
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$userIP = $request->ip();
$anonymousUser = User::where('ipAddress', $userIP)->get();
if (Auth::check() && Auth::user()->isBanned())
{
View::share('isBanned', true);
}
elseif(!$anonymousUser->isEmpty() && User::find($anonymousUser->id)->isBanned())
{
View::share('isBanned', true);
}
else
{
View::share('isBanned', false);
}
return $next($request);
}
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class ForceLogoutMiddleware
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
if (Auth::user()->isBanned())
{
Auth::logout();
$request->session()->flash('error', 'Error: Your session has been forcefully terminated. Please try again in a few days.');
return redirect('/');
}
return $next($request);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class BanUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->hasRole('admin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'reason' => 'required|string',
'durationOperand' => 'nullable|integer',
'durationOperator' => 'nullable|string'
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class DeleteUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->hasRole('admin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'confirmPrompt' => 'required|string'
];
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class NewCommentRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
// TODO: Switch to permission checking when there are comment permission nodes
return Auth::user()->hasAnyRole('reviewer', 'hiringManager', 'admin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'comment' => 'required|string|max:600|min:20'
];
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class SearchPlayerRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->can('admin.userlist');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'searchTerm' => 'required|string|max:17' // max user char limit set by Mojang
];
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
class UpdateUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize()
{
return Auth::user()->hasRole('admin');
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email',
'name' => 'required|string',
'uuid' => 'required|max:32|min:32',
'roles' => 'required_without_all'
];
}
}

56
app/Jobs/CleanBans.php Normal file
View File

@ -0,0 +1,56 @@
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use App\Ban;
use Carbon\Carbon;
class CleanBans implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $bans;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle()
{
Log::debug('Running automatic ban cleaner...');
$bans = Ban::all();
if (!is_null($bans))
{
foreach($this->bans as $ban)
{
$bannedUntil = Carbon::parse($ban->bannedUntil);
if ($bannedUntil->equalTo(now()))
{
Log::debug('Deleted ban ' . $ban->id . ' belonging to ' . $ban->user->name);
$ban->delete();
}
}
}
}
}

View File

@ -3,6 +3,7 @@
namespace App\Listeners;
use App\Events\ApplicationDeniedEvent;
use App\Notifications\ApplicationDenied;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
@ -30,6 +31,7 @@ class DenyUser
$event->application->setStatus('DENIED');
Log::info('User ' . $event->application->user->name . ' just had their application denied.');
// Also dispatch other notifications
$event->application->user->notify(new ApplicationDenied($event->application));
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace App\Listeners;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\Events\UserBannedEvent;
use App\Notifications\UserBanned;
use Illuminate\Support\Facades\Log;
use App\User;
class OnUserBanned
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param object $event
* @return void
*/
public function handle(UserBannedEvent $event)
{
Log::warning("User " . $event->user->name . " has just been banned from the site!");
foreach(User::all() as $user)
{
if ($user->isStaffMember())
{
$user->notify((new UserBanned($event->user, $event->ban))->delay(now()->addSeconds(10)));
}
}
}
}

View File

@ -7,6 +7,9 @@ use Illuminate\Auth\Events\Registered;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use App\User;
use App\Notifications\NewUser;
class OnUserRegistration
{
/**
@ -29,5 +32,13 @@ class OnUserRegistration
{
// TODO: Send push notification to online admins via browser (w/ pusher)
Log::info('User ' . $event->user->name . ' has just registered for an account.');
foreach(User::all() as $user)
{
if ($user->hasRole('admin'))
{
$user->notify(new NewUser($event->user));
}
}
}
}

View File

@ -4,6 +4,7 @@ namespace App\Listeners;
use App\Events\ApplicationApprovedEvent;
use App\StaffProfile;
use App\Notifications\ApplicationApproved;
use Carbon\Carbon;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
@ -37,11 +38,15 @@ class PromoteUser
'memberNotes' => 'Approved by staff members. Welcome them to the team!'
]);
$event->application->user->assignRole('reviewer');
Log::info('User ' . $event->application->user->name . ' has just been promoted!', [
'newRank' => $event->application->response->vacancy->permissionGroupName,
'staffProfileID' => $staffProfile->id
]);
// TODO: Dispatch alert email and notifications for the user and staff members
$event->application->user->notify(new ApplicationApproved($event->application));
// note: Also notify staff
// TODO: Also assign new app role based on the permission group name
}

View File

@ -0,0 +1,93 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use App\Application;
class ApplicationApproved extends Notification implements ShouldQueue
{
use Queueable;
public $application;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Application $application)
{
$this->application = $application;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail', 'slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - ' . $this->application->response->vacancy->vacancyName . ' application approved')
->line('<br />')
->line('Congratulations! Our Staff team has reviewed your application today, and your application has been approved.')
->line('You have just received the Reviewer role, which allows you to view and vote on other applications.')
->line('Your in-game rank should be updated network-wide in the next few minutes, allowing you to perform staff duties.')
->line('Please join a voice channel when possible for your training meeting, if this has been mentioned by your interviewer.')
->line('<br />')
->line('Good luck and welcome aboard!')
->action('Sign in', url(route('login')))
->line('Thank you!');
}
public function toSlack($notifiable)
{
$url = route('showSingleProfile', ['user' => $notifiable->id]);
$roles = implode(', ', $notifiable->roles->pluck('name')->all());
return (new SlackMessage)
->success()
->content('A user has been approved on the team. Welcome aboard!')
->attachment(function($attachment) use ($notifiable, $url, $roles){
$attachment->title('New staff member')
->fields([
'Name' => $notifiable->name,
'Email' => $notifiable->email,
'Roles' => $roles
])
->action('View profile', $url);
});
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use App\Application;
class ApplicationDenied extends Notification implements ShouldQueue
{
use Queueable;
public $application;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Application $application)
{
$this->application = $application;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail', 'slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - ' . $this->application->response->vacancy->vacancyName . ' application denied')
->line('Your most recent application has been denied.')
->line('Our review team denies applications for several reasons, including poor answers.')
->line('Please review your application and try again in 30 days.')
->action('Review application', url(route('showUserApp', ['id' => $this->application->id])))
->line('Better luck next time!');
}
public function toSlack($notifiable)
{
$notifiableName = $notifiable->name;
return (new SlackMessage)
->error()
->content('An application has just been denied.')
->attachment(function($attachment) use ($notifiableName){
$attachment->title('Application denied!')
->content($notifiableName . '\'s application has just been denied. They can try again in 30 days.');
});
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ApplicationMoved extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* 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)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - Application Updated')
->line('Your most recent application has been moved up a stage.')
->line('This means our team has reviewed it and an interview will be scheduled ASAP.')
->action('Sign in', url(route('login')))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class AppointmentFinished extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* 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)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - Appointment completed')
->line('Your appointment has been marked as completed!')
->line('Please allow an additional day for your application to be fully processed.')
->action('View applications', url(route('showUserApps')))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Appointment;
class AppointmentScheduled extends Notification implements ShouldQueue
{
use Queueable;
protected $appointment;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Appointment $appointment)
{
$this->appointment = $appointment;
}
/**
* 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)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - Interview scheduled')
->line('A voice interview has been scheduled for you @ ' . $this->appointment->appointmentDate . '.')
->line('With the following details: ' . $this->appointment->appointmentDescription)
->line('This meeting will take place @ ' . $this->appointment->appointmentLocation . '. You will receive an email soon with details on how to join this meeting.')
->line('You are expected to show up at least 5 minutes before the scheduled date.')
->action('Sign in', url(route('login')))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class ChangedPassword extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* 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)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - Account password changed')
->line('The password for the account registered to this email address has just been changed.')
->line('If this was not you, please contact an administrator immediately.')
->action('Sign in', url(route('login')))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,63 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class EmailChanged extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* 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)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - Email address changed')
->line('The email address for your account has just been updated, either by you or an administrator.')
->action('Sign in', url(route('login')))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Notification;
use App\Application;
use App\Vacancy;
class NewApplicant extends Notification implements ShouldQueue
{
use Queueable;
protected $application;
protected $vacancy;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Application $application, Vacancy $vacancy)
{
$this->application = $application;
$this->vacancy = $vacancy;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['slack'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - New application')
->line('Someone has just applied for a position. Check it out!')
->line('You are receiving this because you\'re a staff member at ' . config('app.name') . '.')
->action('View Application', url(route('showUserApp', ['id' => $this->application->id])))
->line('Thank you!');
}
public function toSlack($notifiable)
{
$vacancyDetails = [];
$vacancyDetails['name'] = $this->vacancy->vacancyName;
$vacancyDetails['slots'] = $this->vacancy->vacancyCount;
$url = route('showUserApp', ['id' => $this->application->id]);
$applicant = $this->application->user->name;
return (new SlackMessage)
->success()
->content('Notice: New application coming through. Please review as soon as possible.')
->attachment(function($attachment) use ($vacancyDetails, $url, $applicant){
$attachment->title('Application details')
->fields([
'Applied for' => $vacancyDetails['name'],
'Avaiable positions' => $vacancyDetails['slots'],
'Applicant' => $applicant
])
->action('Review application', $url);
});
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Comment;
use App\Application;
class NewComment extends Notification implements ShouldQueue
{
use Queueable;
protected $application;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Comment $comment, Application $application)
{
$this->application = $application;
}
/**
* 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)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - New comment')
->line('Someone has just posted a new comment on an application you follow.')
->line('You\'re receiving this email because you\'ve voted/commented on this application.')
->action('Check it out', url(route('showUserApp', ['id' => $this->application->id])))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,100 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\SlackMessage;
use App\User;
use App\Facades\UUID;
class NewUser extends Notification implements ShouldQueue
{
use Queueable;
public $user;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ($notifiable->isStaffMember())
? ['slack', 'mail']
: ['mail'];
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - New user')
->line($this->user->name . ' has just registered to our site.')
->line('You are receiving this email because you opted to receive new user notifications.')
->action('View profile', url(route('showSingleProfile', ['user' => $this->user->id])))
->line('Thank you!');
}
public function toSlack($notifiable)
{
$user = [];
$user['name'] = $this->user->name;
$user['email'] = $this->user->email;
$user['username'] = UUID::toUsername($this->user->uuid);
$date = \Carbon\Carbon::parse($this->user->created_at);
$user['created_at'] = $date->englishMonth . ' ' . $date->day . ' ' . $date->year;
return (new SlackMessage)
->success()
->content('A new user has signed up!')
->attachment(function($attachment) use ($user){
$attachment->title('User details')
->fields([
'Email address' => $user['email'],
'Name' => $user['name'],
'Minecraft Username' => $user['username'],
'Registration date' => $user['created_at']
]);
});
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\User;
use App\Ban;
class UserBanned extends Notification implements ShouldQueue
{
use Queueable;
protected $user;
protected $ban;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(User $user, Ban $ban)
{
$this->user = $user;
$this->ban = $ban;
}
/**
* Get the notification's delivery channels.
*
* @param mixed $notifiable
* @return array
*/
public function via($notifiable)
{
return ['mail']; // slack
}
/**
* Get the mail representation of the notification.
*
* @param mixed $notifiable
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable)
{
return (new MailMessage)
->from(config('notification.sender.address'), config('notification.sender.name'))
->line('Hello, ')
->line('Moderators have just banned user ' . $this->user->name . ' for ' . $this->ban->reason)
->line('This ban will remain in effect until ' . $this->ban->bannedUntil . '.')
->action('View profile', url(route('showSingleProfile', ['user' => $this->user->id])))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,68 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\Vacancy;
class VacancyClosed extends Notification implements ShouldQueue
{
use Queueable;
protected $vacancy;
/**
* Create a new notification instance.
*
* @return void
*/
public function __construct(Vacancy $vacancy)
{
$this->vacancy = $vacancy;
}
/**
* 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)
->from(config('notification.sender.address'), config('notification.sender.name'))
->subject(config('app.name') . ' - Vacancy Closed')
->line('The vacancy ' . $this->vacancy->vacancyName . ', with ' . $this->vacancy->vacancyCount . ' remaining slots, has just been closed.')
->line('Please be aware that this position may be deleted/reopened any time.')
->action('View positions', url(route('showPositions')))
->line('Thank you!');
}
/**
* Get the array representation of the notification.
*
* @param mixed $notifiable
* @return array
*/
public function toArray($notifiable)
{
return [
//
];
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace App\Observers;
use App\Profile;
use App\User;
use Illuminate\Support\Facades\Log;
class UserObserver
{
/**
* Handle the user "created" event.
*
* @param \App\User $user
* @return void
*/
public function created(User $user)
{
Profile::create([
'profileShortBio' => 'Write a one-liner about you here!',
'profileAboutMe' => 'Tell us a bit about you.',
'socialLinks' => '{}',
'userID' => $user->id
]);
}
/**
* Handle the user "updated" event.
*
* @param \App\User $user
* @return void
*/
public function updated(User $user)
{
//
}
public function deleting(User $user)
{
$user->profile()->delete();
Log::debug('Referential integrity cleanup: Deleted profile!');
$applications = $user->applications;
if (!$applications->isEmpty())
{
Log::debug('RIC: Now trying to delete applications and responses...');
foreach($applications as $application)
{
$application->response()->delete();
$votes = $application->votes;
foreach ($votes as $vote)
{
Log::debug('RIC: Deleting and detaching vote ' . $vote->id);
$vote->application()->detach($application->id);
$vote->delete();
}
if (!is_null($application->appointment))
{
Log::debug('RIC: Deleting appointment!');
$application->appointment()->delete();
}
if (!$application->comments->isEmpty())
{
Log::debug('RIC: Deleting comments!');
foreach($application->comments as $comment)
{
$comment->delete();
}
}
Log::debug('RIC: Deleting application ' . $application->id);
$application->delete();
}
}
Log::debug('RIC: Cleanup done!');
}
/**
* Handle the user "deleted" event.
*
* @param \App\User $user
* @return void
*/
public function deleted(User $user)
{
//
}
/**
* Handle the user "restored" event.
*
* @param \App\User $user
* @return void
*/
public function restored(User $user)
{
//
}
/**
* Handle the user "force deleted" event.
*
* @param \App\User $user
* @return void
*/
public function forceDeleted(User $user)
{
//
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Policies;
use App\Application;
use Illuminate\Auth\Access\Response;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ApplicationPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
public function view(User $user, Application $application)
{
if ($user->is($application->user) && $user->can('applications.view.own') || $user->can('applications.view.all'))
{
return Response::allow();
}
return Response::deny('You are not authorised to view this application');
}
}

View File

@ -2,6 +2,7 @@
namespace App\Policies;
use App\Profile;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
@ -16,6 +17,11 @@ class ProfilePolicy
*/
public function __construct()
{
//
}
public function edit(User $user, Profile $profile)
{
return $user->is($profile->user);
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Policies;
use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class UserPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
}
public function edit(User $authUser, User $user)
{
return $authUser->is($user) || $authUser->hasRole('admin');
}
public function viewStaff(User $user)
{
return $user->can('admin.stafflist');
}
public function viewPlayers(User $user)
{
return $user->can('admin.userlist');
}
public function terminate(User $authUser)
{
return $authUser->hasRole('admin');
}
}

View File

@ -2,6 +2,8 @@
namespace App\Providers;
use App\Observers\UserObserver;
use App\User;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
@ -25,5 +27,6 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
Schema::defaultStringLength(191);
User::observe(UserObserver::class);
}
}

View File

@ -2,6 +2,10 @@
namespace App\Providers;
use App\Http\Controllers\ProfileController;
use App\Policies\ProfilePolicy;
use App\Policies\UserPolicy;
use App\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
@ -14,6 +18,9 @@ class AuthServiceProvider extends ServiceProvider
*/
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
'App\Application' => 'App\Policies\ApplicationPolicy',
ProfileController::class => ProfilePolicy::class,
User::class => UserPolicy::class
];
/**

View File

@ -25,6 +25,9 @@ class EventServiceProvider extends ServiceProvider
],
'App\Events\ApplicationDeniedEvent' => [
'App\Listeners\DenyUser'
],
'App\Events\UserBannedEvent' => [
'App\Listeners\OnUserBanned'
]
];

View File

@ -0,0 +1,33 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App;
class IPInfoProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
App::bind('ipInformationFacade', function(){
return new App\CustomFacades\IP();
});
}
}

View File

@ -31,7 +31,7 @@ class RouteServiceProvider extends ServiceProvider
public function boot()
{
//
Route::model('user', \App\User::class);
parent::boot();
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App;
class UUIDConversionProvider extends ServiceProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
App::bind('uuidConversionFacade', function(){
return new App\UUID\UUID();
});
}
}

View File

@ -12,6 +12,7 @@ class Response extends Model
'responseData'
];
public function form()
{
return $this->hasOne('App\Form', 'id', 'responseFormID');

53
app/UUID/UUID.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace App\UUID;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class UUID
{
// Caching would not be needed here since this method won't be used in pages that loop over a collection of usernames.
public function toUUID($username)
{
if (is_null($username))
{
throw new \LogicException('Argument username for ' . __METHOD__ . ' cannot be null!');
}
$response = json_decode(Http::post(config('general.urls.mojang.api') . '/profiles/minecraft', [
$username
])->body(), true);
return $response[0]['id'];
}
// Note: Caching could simply be assigning the username to it's UUID, however, to make this work, we'd need to loop over all cache items, which would be slighly ineffective
public function toUsername($uuid)
{
if (is_null($uuid))
{
throw new \LogicException('Argument uuid for ' . __METHOD__ . ' cannot be null!');
}
$shortUUID = substr($uuid, 0, 8);
$username = Cache::remember('uuid_' . $shortUUID, now()->addDays(30), function() use ($shortUUID, $uuid) {
$response = json_decode(Http::get(config('general.urls.mojang.session') . '/session/minecraft/profile/' . $uuid)->body(), true);
Log::debug('Caching ' . $shortUUID . 'for thirty days');
return $response['name'];
});
return $username;
}
}

View File

@ -5,10 +5,13 @@ namespace App;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
use Notifiable;
use HasRoles;
//use MustVerifyEmail;
/**
* The attributes that are mass assignable.
@ -37,6 +40,8 @@ class User extends Authenticatable
'email_verified_at' => 'datetime',
];
//
public function applications()
{
return $this->hasMany('App\Application', 'applicantUserID', 'id');
@ -52,4 +57,33 @@ class User extends Authenticatable
return $this->hasOne('App\Profile', 'userID', 'id');
}
public function bans()
{
return $this->hasOne('App\Ban', 'userID', 'id');
}
public function comments()
{
return $this->hasMany('App\Comment', 'authorID', 'id');
}
public function isBanned()
{
return !$this->bans()->get()->isEmpty();
}
public function isStaffMember()
{
return $this->hasAnyRole('reviewer', 'admin', 'hiringManager');
}
public function routeNotificationForSlack($notification)
{
return config('slack.webhook.integrationURL');
}
}

View File

@ -10,15 +10,18 @@
"require": {
"php": "^7.2.5",
"ext-json": "*",
"arcanedev/log-viewer": "^7.0",
"doctrine/dbal": "^2.10",
"fideloper/proxy": "^4.2",
"fruitcake/laravel-cors": "^1.0",
"guzzlehttp/guzzle": "^6.5",
"jeroennoten/laravel-adminlte": "^3.2",
"laravel/framework": "^7.0",
"laravel/slack-notification-channel": "^2.0",
"laravel/tinker": "^2.0",
"laravel/ui": "^2.0",
"sentry/sentry-laravel": "1.7.1"
"sentry/sentry-laravel": "1.7.1",
"spatie/laravel-permission": "^3.13"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.3",

1221
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -45,12 +45,12 @@ return [
|
*/
'logo' => '<b>Admin</b>LTE',
'logo_img' => 'vendor/adminlte/dist/img/AdminLTELogo.png',
'logo' => 'RaspberryNet Staff',
'logo_img' => 'https://www.raspberrypi.org/app/uploads/2020/05/Raspberry-Pi-OS-downloads-image-150x150-1.png',
'logo_img_class' => 'brand-image img-circle elevation-3',
'logo_img_xl' => null,
'logo_img_xl_class' => 'brand-image-xs',
'logo_img_alt' => 'AdminLTE',
'logo_img_alt' => 'Raspberry Network Staff Temporary Logo',
/*
|--------------------------------------------------------------------------
@ -208,10 +208,14 @@ return [
*/
'menu' => [
'Applications',
[
'header' => 'Applications',
'can' => 'applications.view.own'
],
[
'text' => 'My Applications',
'icon' => 'fas fa-fw fa-list-ul',
'can' => 'applications.view.own',
'submenu' => [
[
'text' => 'Current Applications',
@ -232,36 +236,54 @@ return [
'icon' => 'fas fa-user-circle',
'url' => '/profile/settings/account'
],
'Application Management',
[
'header' => 'Application Management',
'can' => ['applications.view.all', 'applications.vote']
],
[
'text' => 'Outstanding Applications',
'url' => '/applications/staff/outstanding',
'icon' => 'far fa-folder-open'
'icon' => 'far fa-folder-open',
'can' => 'applications.view.all'
],
[
'text' => 'Interview Queue',
'url' => '/applications/staff/pending-interview',
'icon' => 'fas fa-fw fa-microphone-alt'
'icon' => 'fas fa-fw fa-microphone-alt',
'can' => 'applications.view.all'
],
[
'text' => 'Peer Approval Queue',
'url' => '/applications/staff/peer-review',
'icon' => 'fas fa-fw fa-search'
'icon' => 'fas fa-fw fa-search',
'can' => 'applications.view.all'
],
[
'header' => 'Administration',
'can' => [ // may need to be modified
'admin.hiring.*',
'admin.userlist',
'admin.stafflist',
'admin.hiring.*',
'admin.notificationsettings.*'
]
],
'Administration',
[
'text' => 'Staff Members',
'icon' => 'fas fa-fw fa-users',
'url' => '/hr/staff-members'
'url' => '/hr/staff-members',
'can' => 'admin.stafflist'
],
[ // players who haven't been promoted yet
'text' => 'Registered Players',
'icon' => 'fas fa-fw fa-user-friends',
'url' => '/hr/players'
'url' => '/hr/players',
'can' => 'admin.userlist'
],
[
'text' => 'Hiring Management',
'icon' => 'far fa-calendar-plus',
'can' => 'admin.hiring.*',
'submenu' => [
[
'text' => 'Open Positions',
@ -289,23 +311,27 @@ return [
[
'text' => 'App Settings',
'icon' => 'fas fa-fw fa-cog',
'can' => 'admin.notificationsettings',
'submenu' => [
[
'text' => 'Global Notification Settings',
'icon' => 'far fa-bell',
'url' => '/admin/notifications'
'url' => '/admin/notifications',
'can' => 'admin.notificationsettings.edit'
],
[
'text' => 'Developer Tools',
'icon' => 'fas fa-code',
'url' => '/admin/devtools'
]
'url' => '/admin/devtools',
'can' => 'admin.developertools.use'
]
]
],
[
'text' => 'Activity Logs',
'url' => '/admin/logs',
'icon' => 'fas fa-clipboard-list'
'text' => 'System Logs',
'url' => '/admin/maintenance/system-logs',
'icon' => 'fas fa-clipboard-list',
'can' => 'admin.maintenance.logs.view'
]
],

View File

@ -171,6 +171,8 @@ return [
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\UUIDConversionProvider::class,
App\Providers\IPInfoProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
@ -227,6 +229,8 @@ return [
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'UUID' => App\Facades\UUID::class,
'IP' => App\Facades\IP::class
],

View File

@ -8,9 +8,20 @@ return [
'mojang' => [
'statuscheck' => env('MOJANG_STATUS_URL') ?? 'https://status.mojang.com/check',
'api' => env('MOJANG_API_URL') ?? ' https://api.mojang.com'
'api' => env('MOJANG_API_URL') ?? ' https://api.mojang.com',
'session' => env('MOJANG_SESSIONAPI_URL') ?? 'https://sessionserver.mojang.com'
],
'ipapi' => [
'ipcheck' => env('IPGEO_API_URL') ?? 'https://api.ipgeolocation.io/ipgeo'
]
],
'keys' => [
'ipapi' => [
'apikey' => env('IPGEO_API_KEY')
]
]
];

141
config/log-viewer.php Normal file
View File

@ -0,0 +1,141 @@
<?php
use Arcanedev\LogViewer\Contracts\Utilities\Filesystem;
return [
/* -----------------------------------------------------------------
| Log files storage path
| -----------------------------------------------------------------
*/
'storage-path' => storage_path('logs'),
/* -----------------------------------------------------------------
| Log files pattern
| -----------------------------------------------------------------
*/
'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'
],
/* -----------------------------------------------------------------
| 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'
*/
'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,
],
],
/* -----------------------------------------------------------------
| 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:',
],
];

12
config/notification.php Normal file
View File

@ -0,0 +1,12 @@
<?php
return [
'sender' => [
'address' => 'teams@spacejewel-hosting.com',
'name' => 'RaspberryNet Teams'
]
];

143
config/permission.php Normal file
View File

@ -0,0 +1,143 @@
<?php
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Spatie\Permission\Models\Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Spatie\Permission\Models\Role::class,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
],
/*
* When set to true, the required permission names are added to the exception
* message. This could be considered an information leak in some contexts, so
* the default setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to the exception
* message. This could be considered an information leak in some contexts, so
* the default setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
*/
'enable_wildcard_permission' => true,
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* When checking for a permission against a model by passing a Permission
* instance to the check, this key determines what attribute on the
* Permissions model is used to cache against.
*
* Ideally, this should match your preferred way of checking permissions, eg:
* `$user->can('view-posts')` would be 'name'.
*/
'model_key' => 'name',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];

12
config/slack.php Normal file
View File

@ -0,0 +1,12 @@
<?php
return [
'webhook' => [
'integrationURL' => env('SLACK_INTEGRATION_WEBHOOK')
]
];

View File

@ -27,8 +27,7 @@ class CreateProfilesTable extends Migration
$table->foreign('userID')
->references('id')
->on('users')
->onDelete('cascade');
->on('users');
});
}

View File

@ -31,6 +31,10 @@ class CreateApplicationsTable extends Migration
'DENIED'
])->default('STAGE_SUBMITTED');
$table->timestamps();
$table->foreign('applicantUserID')
->references('id')
->on('users');
});
}

View File

@ -22,7 +22,9 @@ class CreateVotesTable extends Migration
]);
$table->timestamps();
$table->foreign('userID')->references('id')->on('users');
$table->foreign('userID')
->references('id')
->on('users');
});
}

View File

@ -32,6 +32,10 @@ class CreateAppointmentsTable extends Migration
$table->boolean('userAccepted')->default(false);
$table->longText('meetingNotes')->nullable();
$table->timestamps();
$table->foreign('applicationID')
->references('id')
->on('applications');
});
}

View File

@ -23,7 +23,10 @@ class CreateVacanciesTable extends Migration
$table->integer('vacancyCount')->default(3);
$table->timestamps();
$table->foreign('vacancyFormID')->references('id')->on('forms');
$table->foreign('vacancyFormID')
->references('id')
->on('forms');
});
}

View File

@ -21,6 +21,10 @@ class CreateStaffProfilesTable extends Migration
$table->dateTime('resignationDate')->nullable();
$table->text('memberNotes')->nullable();
$table->timestamps();
$table->foreign('userID')
->references('id')
->on('users');
});
}

View File

@ -18,6 +18,12 @@ class CreateResponsesTable extends Migration
$table->bigInteger('responseFormID')->unsigned();
$table->longText('responseData');
$table->timestamps();
// A better way would be to link responses directly to vacancies, that subsquently have a form
$table->foreign('responseFormID')
->references('id')
->on('forms');
});
}

View File

@ -25,8 +25,5 @@ class ChangeFormStructureLength extends Migration
*/
public function down()
{
Schema::table('forms', function (Blueprint $schema){
$schema->string('formStructure')->change();
});
}
}

View File

@ -0,0 +1,110 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreatePermissionTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
Schema::create($tableNames['permissions'], function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->timestamps();
});
Schema::create($tableNames['roles'], function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('guard_name');
$table->timestamps();
});
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
$table->unsignedBigInteger('permission_id');
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign('permission_id')
->references('id')
->on($tableNames['permissions'])
->onDelete('cascade');
$table->primary(['permission_id', $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
});
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
$table->unsignedBigInteger('role_id');
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign('role_id')
->references('id')
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary(['role_id', $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) {
$table->unsignedBigInteger('permission_id');
$table->unsignedBigInteger('role_id');
$table->foreign('permission_id')
->references('id')
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign('role_id')
->references('id')
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary(['permission_id', 'role_id'], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
}

View File

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateBansTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('bans', function (Blueprint $table) {
$table->id();
$table->bigInteger('userID')->unsigned();
$table->string('reason');
$table->timestamp('bannedUntil')->nullable();
$table->string('userAgent');
$table->bigInteger('authorUserID');
$table->timestamps();
$table->foreign('userID')
->references('id')
->on('users');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('bans');
}
}

View File

@ -0,0 +1,44 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCommentsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->bigInteger('authorID')->unsigned();
$table->bigInteger('applicationID')->unsigned();
$table->mediumText('text');
$table->timestamps();
$table->foreign('authorID')
->references('id')
->on('users');
$table->foreign('applicationID')
->references('id')
->on('applications');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('comments');
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateJobsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('jobs');
}
}

View File

@ -11,6 +11,7 @@ class DatabaseSeeder extends Seeder
*/
public function run()
{
// $this->call(UserSeeder::class);
$this->call(PermissionSeeder::class);
$this->call(UserSeeder::class);
}
}

View File

@ -0,0 +1,98 @@
<?php
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class PermissionSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
//
$user = Role::create(
[
'name' => 'user'
]
);
$reviewer = Role::create(
[
'name' => 'reviewer'
]
);
$hiringManager = Role::create(
[
'name' => 'hiringManager'
]
);
$admin = Role::create([
'name' => 'admin'
]);
// Spatie wildcard permissions (same concept of MC permissions)
Permission::create(['name' => 'applications.submit']);
Permission::create(['name' => 'applications.stages.deny']);
Permission::create(['name' => 'applications.stages.approve']);
Permission::create(['name' => 'applications.view.all']);
Permission::create(['name' => 'applications.view.own']);
Permission::create(['name' => 'applications.vote']);
Permission::create(['name' => 'appointments.schedule']);
Permission::create(['name' => 'appointments.schedule.edit']);
Permission::create(['name' => 'appointments.schedule.cancel']);
Permission::create(['name' => 'applications.*']);
Permission::create(['name' => 'appointments.*']);
Permission::create(['name' => 'profiles.view.others']);
Permission::create(['name' => 'profiles.edit.others']);
Permission::create(['name' => 'admin.userlist']);
Permission::create(['name' => 'admin.stafflist']);
Permission::create(['name' => 'admin.hiring.forms']);
Permission::create(['name' => 'admin.hiring.formbuilder']);
Permission::create(['name' => 'admin.hiring.vacancy']);
Permission::create(['name' => 'admin.hiring.vacancy.edit,delete']);
Permission::create(['name' => 'admin.notificationsettings']);
Permission::create(['name' => 'admin.notificationsettings.edit']);
Permission::create(['name' => 'admin.hiring.*']);
Permission::create(['name' => 'admin.notificationsettings.*']);
Permission::create(['name' => 'admin.maintenance.logs.view']);
Permission::create(['name' => 'admin.developertools.use']);
$user->givePermissionTo([
'applications.submit',
'applications.view.own',
'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'
]);
$hiringManager->givePermissionTo('appointments.*', 'applications.*', 'admin.hiring.*');
$admin->givePermissionTo([
'appointments.*',
'admin.userlist',
'admin.stafflist',
'admin.hiring.*',
'admin.notificationsettings.*',
'profiles.view.others',
'profiles.edit.others',
'admin.maintenance.logs.view'
]);
}
}

View File

@ -0,0 +1,146 @@
<?php
use App\Profile;
use App\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$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',
'name' => 'Admin',
'email' => 'admin@example.com',
'username' => 'admin',
'originalIP' => '192.168.1.2',
'password' => Hash::make('password')
]);
foreach (User::all() as $user)
{
$user->assignRole('reviewer', 'user');
}
}
}

38
public/css/comments.css vendored Normal file
View File

@ -0,0 +1,38 @@
blockquote {
background: #f9f9f9;
border-left: 10px solid #ccc;
margin: 1.5em 10px;
padding: 0.5em 10px;
quotes: "\201C""\201D""\2018""\2019";
}
blockquote:before {
color: #ccc;
content: open-quote;
font-size: 4em;
line-height: 0.1em;
margin-right: 0.25em;
vertical-align: -0.4em;
}
blockquote p {
display: inline;
}
.comment {
border-radius: 30px;
}
.comment-header {
border: none;
}
.comment-footer {
border: none;
}
.commenter {
font-size: large;
color: darkgrey;
font-weight: bold;
}

3
public/css/picker.css vendored Normal file
View File

@ -0,0 +1,3 @@
.flatpickr-calendar.open {
z-index: 10000;
}

12223
public/js/app.js vendored

File diff suppressed because it is too large Load Diff

30
resources/js/app.js vendored
View File

@ -7,10 +7,6 @@
require('chart.js');
require('./bootstrap');
import { Calendar } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import timeGridPlugin from '@fullcalendar/timegrid';
import listPlugin from '@fullcalendar/list';
import flatpickr from "flatpickr";
@ -21,6 +17,32 @@ flatpickr("#appointmentDateTime", {
static: false
});
$("#banAccountTrigger").on("click", function(event){
$("#banAccountModal").modal('show');
});
$("#durationDropdown").dropdown();
$(".dropdown-menu a").on("click", function(e){
$(".duration-btn").text(this.innerHTML);
$("#operator").val(this.innerHTML);
});
$("#banAccountButton").on("click", function(){
$("#banAccountForm").submit();
});
$("#comment").keyup(function(){
$("#charcount").text($("#comment").val().length);
});
$("#submitComment").on('click', function(){
$("#newComment").submit();
});
window.Vue = require('vue');

View File

@ -0,0 +1,13 @@
{
"Date": "تاريخ",
"The list of logs is empty!": "قائمة سجلات فارغة!",
"All": "الجميع",
"Emergency": "حالات الطوارئ",
"Alert": "إنذار",
"Critical": "حرج",
"Error": "خطأ",
"Warning": "تحذير",
"Notice": "ملاحظة",
"Info": "المعلومات",
"Debug": "التصحيح"
}

View File

@ -0,0 +1,13 @@
{
"Date": "Дата",
"The list of logs is empty!": "Не са намерени логове!",
"All": "Всички",
"Emergency": "Emergency",
"Alert": "Alert",
"Critical": "Critical",
"Error": "Error",
"Warning": "Warning",
"Notice": "Notice",
"Info": "Info",
"Debug": "Debug"
}

View File

@ -0,0 +1,13 @@
{
"Date": "Datum",
"The list of logs is empty!": "KeineLogDateiengefunden!",
"All": "Alle",
"Emergency": "Notfall",
"Alert": "Alarm",
"Critical": "Kritisch",
"Error": "Fehler",
"Warning": "Warnung",
"Notice": "Hinweis",
"Info": "Info",
"Debug": "Debug"
}

View File

@ -0,0 +1,13 @@
{
"Date": "Fecha",
"The list of logs is empty!": "La lista del log está vacía!",
"All": "Todos",
"Emergency": "Emergencia",
"Alert": "Alerta",
"Critical": "Criticos",
"Error": "Errores",
"Warning": "Advertencia",
"Notice": "Aviso",
"Info": "Info",
"Debug": "Debug"
}

View File

@ -0,0 +1,13 @@
{
"Date": "Kuupäev",
"The list of logs is empty!": "Logide nimekiri on tühi!",
"All": "Kõik",
"Emergency": "Erakorraline",
"Alert": "Häire",
"Critical": "Kriitiline",
"Error": "Viga",
"Warning": "Hoiatus",
"Notice": "Teade",
"Info": "Info",
"Debug": "Silumine"
}

View File

@ -0,0 +1,13 @@
{
"Date": "تاریخ",
"The list of logs is empty!": "چیزی برای نمایش وجود ندارد!",
"All": "همه",
"Emergency": "اورژانسی",
"Alert": "اخطار",
"Critical": "بحرانی",
"Error": "خطا",
"Warning": "هشدار",
"Notice": "اعلان",
"Info": "اطلاعات",
"Debug": "دیباگ"
}

View File

@ -0,0 +1,13 @@
{
"Date": "Date",
"The list of logs is empty!": "La liste des logs est vide!",
"All": "Tous",
"Emergency": "Urgence",
"Alert": "Alerte",
"Critical": "Critique",
"Error": "Erreur",
"Warning": "Avertissement",
"Notice": "Notice",
"Info": "Info",
"Debug": "Debug"
}

View File

@ -0,0 +1,13 @@
{
"Date": "Dátum",
"The list of logs is empty!": "A naplók listája üres!",
"All": "Összes",
"Emergency": "Vészhelyzet",
"Alert": "Riasztás",
"Critical": "Kritikus",
"Error": "Hiba",
"Warning": "Figyelmeztetés",
"Notice": "Értesítés",
"Info": "Információ",
"Debug": "Hibakeresés"
}

View File

@ -0,0 +1,13 @@
{
"Date": "Ամսաթիվ",
"The list of logs is empty!": "Լոգերի ցուցակը դատարկ է։",
"All": "Բոլորը",
"Emergency": "Վթարային",
"Alert": "Նախազգուշացում",
"Critical": "Կրիտիկական",
"Error": "Սխալ",
"Warning": "Նախազգուշացում",
"Notice": "Ծանուցում",
"Info": "Տեղեկատվություն",
"Debug": "Կարգաբերում"
}

Some files were not shown because too many files have changed in this diff Show More