Commit 57296b80 authored by Jack Stupple's avatar Jack Stupple

admin area for resources

parent fb0f02a7
......@@ -5,3 +5,4 @@ Homestead.yaml
.env
.htpasswd
.DS_Store
/node_modules
\ No newline at end of file
RewriteEngine on
RewriteRule . public/index.php [L]
RewriteEngine On
RewriteCond %{THE_REQUEST} /public/([^\s?]*) [NC]
RewriteRule ^ %1 [L,NE,R=302]
RewriteRule ^((?!public/).*)$ public/$1 [L,NC]
\ No newline at end of file
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AdminPasswordResetToken extends Model
{
public function user()
{
return $this->belongsTo(AdminUser::class);
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AdminUser extends Model
{
public function userGroup()
{
return $this->belongsTo('App\AdminUserGroup');
}
public function passwordResetTokens()
{
return $this->hasMany(AdminPasswordResetToken::class);
}
public function createPasswordResetToken()
{
$this->passwordResetTokens()->delete();
$token = new AdminPasswordResetToken;
$token->admin_user_id = $this->id;
$token->token = random_str(32);
$token->save();
return $token;
}
}
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AdminUserGroup extends Model
{
public function users()
{
return $this->hasMany('App\AdminUser');
}
}
<?php
namespace App\Console\Commands\Make;
use App\AdminUser;
use App\AdminUserGroup;
use Illuminate\Console\Command;
class MakeAdminUser extends Command
{
protected $signature = 'make:admin-user {--E|email= : User\'s login email.} {--N|name= : Friendly name for the user.}';
protected $description = 'Create a new admin user.';
public function handle()
{
$user = new AdminUser();
$user->full_name = $this->option('name') ?? $this->output->ask('What is the users full name?');;
$user->email = $this->option('email') ?? $this->output->ask('What is the users email address?');;
$user_groups_formatted = [];
$user_groups = AdminUserGroup::all();
$user_groups->each(function ($user_group) use (&$user_groups_formatted) {
$user_groups_formatted[$user_group->id] = $user_group->name;
});
$user_group_name = $this->output->choice('What is their user group?', $user_groups_formatted, array_first($user_groups_formatted));
$user->admin_user_group_id = array_search($user_group_name, $user_groups_formatted);
$password = str_random(16);
$user->password = password_hash($password, PASSWORD_DEFAULT);
$user->save();
$this->output->writeln('<info>User created</info>');
$this->output->writeln("<comment>User Email:</comment> {$user->email}");
$this->output->writeln("<comment>User Password:</comment> {$password}");
}
}
\ No newline at end of file
......@@ -4,6 +4,7 @@ namespace App\Console;
use App\Console\Commands\AddCurrency;
use App\Console\Commands\ImportResources;
use App\Console\Commands\Make\MakeAdminUser;
use Illuminate\Console\Scheduling\Schedule;
use Laravel\Lumen\Console\Kernel as ConsoleKernel;
......@@ -16,7 +17,8 @@ class Kernel extends ConsoleKernel
*/
protected $commands = [
ImportResources::class,
AddCurrency::class
AddCurrency::class,
MakeAdminUser::class
];
/**
......
<?php
function is_logged_in()
{
return !empty($_SESSION['user_id']);
}
function is_logged_in_admin()
{
return !empty($_SESSION['admin_user_id']);
}
\ No newline at end of file
<?php
namespace App\Helpers;
class FlashMessage
{
protected static $identifier = 'flash-message';
public static function count()
{
return count($_SESSION[static::$identifier]);
}
public static function fetch()
{
$messages = $_SESSION[static::$identifier];
$_SESSION[static::$identifier] = [];
return collect($messages);
}
public static function destroy()
{
unset($_SESSION[static::$identifier]);
return true;
}
public static function init()
{
if (!isset($_SESSION[static::$identifier])) {
$_SESSION[static::$identifier] = [];
}
}
public function __construct($message, $type = 'info')
{
$_SESSION[static::$identifier][] = (object) compact('message', 'type');
}
}
\ No newline at end of file
<?php
/**
* @param $partial_name
* @param array $data
* @return string
*/
function get_partial($partial_name, array $data = [])
{
$partial_file = resource_path('partials') . DIRECTORY_SEPARATOR . $partial_name . '.php';
if (!file_exists($partial_file)) {
trigger_error('Partial not found: ' . $partial_file, E_USER_NOTICE);
return '';
}
ob_start();
extract($data);
include $partial_file;
return ob_get_clean();
}
\ No newline at end of file
<?php
/**
* Generate a random string, using a cryptographically secure
* pseudorandom number generator (random_int)
*
* For PHP 7, random_int is a PHP core function
* For PHP 5.x, depends on https://github.com/paragonie/random_compat
*
* @param int $length How many characters do we want?
* @param string $keyspace A string of all possible characters to select from
* @return string
* @throws Exception
*
* @see https://stackoverflow.com/questions/4356289/php-random-string-generator
*/
function random_str($length, $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
{
$pieces = [];
$max = mb_strlen($keyspace, '8bit') - 1;
for ($i = 0; $i < $length; ++$i) {
$pieces []= $keyspace[random_int(0, $max)];
}
return implode('', $pieces);
}
\ No newline at end of file
<?php
function time_ago($datetime, $full = false) {
$now = new DateTime;
$ago = is_a($datetime, \DateTime::class) ? $datetime : new DateTime($datetime);
$diff = $now->diff($ago);
$diff->w = floor($diff->d / 7);
$diff->d -= $diff->w * 7;
$string = array(
'y' => 'year',
'm' => 'month',
'w' => 'week',
'd' => 'day',
'h' => 'hour',
'i' => 'minute',
's' => 'second',
);
foreach ($string as $k => &$v) {
if ($diff->$k) {
$v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
} else {
unset($string[$k]);
}
}
if (!$full) $string = array_slice($string, 0, 1);
if (!$string) {
return 'just now';
}
if ($now < $ago) {
return 'in ' . implode(', ', $string);
} else {
return implode(', ', $string) . ' ago';
}
}
\ No newline at end of file
<?php
namespace App\Http\Controllers\Admin;
use App\AdminPasswordResetToken;
use App\AdminUser;
use App\Helpers\FlashMessage;
use App\Jobs\PasswordResetEmail;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Queue;
use Illuminate\Validation\ValidationException;
class AdminLoginController extends Controller
{
protected static $invalid_password = 'Your email or password is incorrect.';
public function index()
{
return view('admin/login/form');
}
public function verifyLogin(Request $request)
{
$admin_user = AdminUser::where('email', $request->input('email'))->first();
// user is invalid
if (!$admin_user || !password_verify($request->input('password'), $admin_user->password)) {
new FlashMessage(_(static::$invalid_password), 'danger');
return redirect('/admin/login');
}
$_SESSION['admin_user_id'] = $admin_user->id;
new FlashMessage('You have successfully logged in.', 'success');
return redirect('/admin');
}
public function destroy()
{
session_unset();
new \App\Helpers\FlashMessage('You have successfully been logged out.', 'success');
return redirect('/admin/login');
}
public function forgotPassword()
{
static::title('Forgot password');
return view('/admin/login/forgot-password');
}
public function verifyForgotPassword(Request $request)
{
$admin_user = AdminUser::where('email', $request->input('email'))->first();
// user is invalid
if (!$admin_user) {
new FlashMessage('You should expect an email soon.', 'success');
return redirect('/admin/login/forgot-password');
}
$token = $admin_user->createPasswordResetToken();
new FlashMessage('You should expect an email soon.', 'success');
Queue::push(new PasswordResetEmail($admin_user, \Illuminate\Support\Facades\Request::root() . '/admin/login/new-password/' . $token->token));
return redirect('/admin');
}
public function newPassword(Request $request, $token)
{
// validate token exists
$token = AdminPasswordResetToken::where('token', $token)->firstOrfail();
return view('/admin/login/new-password', compact('errors'));
}
public function verifyNewPassword(Request $request, $token)
{
try {
$this->validate($request, [
'password' => 'required|min:8|max:255|confirmed'
]);
} catch (ValidationException $exception) {
foreach ($exception->errors() as $error_field => $errors) {
foreach ($errors as $error) {
new FlashMessage($error, 'danger');
}
}
return $this->newPassword($request, $token);
}
$token = AdminPasswordResetToken::where('token', $token)->firstOrfail();
$user = AdminUser::where('id', $token->admin_user_id)->firstOrFail();
$user->password = password_hash($request->input('password'), PASSWORD_DEFAULT);
$user->save();
new FlashMessage('Password updated.', 'success');
return redirect('/admin/login');
}
}
<?php
namespace App\Http\Controllers\Admin;
use Laravel\Lumen\Routing\Controller as BaseController;
class Controller extends BaseController {
protected static $_title;
protected static $_description;
public static function title($new_title = '')
{
if ($new_title) {
static::$_title = $new_title;
}
return static::$_title;
}
public static function description($new_title = '')
{
if ($new_title) {
static::$_description = $new_title;
}
return static::$_title;
}
}
\ No newline at end of file
<?php
namespace App\Http\Controllers\Admin;
use App\Language;
use App\Resource;
use App\ResourceCategory;
use Illuminate\Http\Request;
class ResourcesController extends Controller {
public function index()
{
$resources = Resource::with(['categories'])->get();
$resource_categories = ResourceCategory::all();
$languages = Language::all();
$content = get_partial('admin/resources/list', compact('resources', 'resource_categories', 'languages'));
return view('admin/index', compact('content'));
}
public function submit(Request $request)
{
foreach ($request->input() as $_resource) {
$resource = new Resource;
$resource->language_id = $_resource['locale'];
$resource->name = $_resource['name'];
$resource->link = $_resource['link'];
$resource->save();
$resource->categories()->attach($_resource['category']);
}
return redirect('/admin/resources');
}
}
\ No newline at end of file
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
class AlreadyAuthenticatedAdmin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (is_logged_in_admin()) {
return redirect('/admin');
}
return $next($request);
}
}
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Contracts\Auth\Factory as Auth;
class AuthenticateAdmin
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $guard
* @return mixed
*/
public function handle($request, Closure $next, $guard = null)
{
if (!is_logged_in_admin()) {
return redirect('/admin/login');
}
return $next($request);
}
}
<?php
namespace App\Http\Middleware;
use Closure;
class Session
{
public function handle($request, Closure $next)
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
\App\Helpers\FlashMessage::init();
return $next($request);
}
}
\ No newline at end of file
......@@ -12,4 +12,9 @@ class Resource extends Model
return $this->belongsToMany(\App\ResourceCategory::class);
}
public function language()
{
return $this->belongsTo(\App\Language::class);
}
}
\ No newline at end of file
......@@ -13,7 +13,7 @@ class ResourceCategory extends Model
public function resources()
{
return $this->hasMany(\App\Resource::class);
return $this->belongsToMany(\App\Resource::class);
}
public function answers()
......
......@@ -64,7 +64,9 @@ $app->middleware([
]);
$app->routeMiddleware([
'auth' => App\Http\Middleware\Authenticate::class,
'authenticateAdmin' => App\Http\Middleware\AuthenticateAdmin::class,
'alreadyAuthenticatedAdmin' => App\Http\Middleware\AlreadyAuthenticatedAdmin::class,
'session' => App\Http\Middleware\Session:: class
]);
/*
......@@ -100,4 +102,12 @@ $app->router->group([
require __DIR__.'/../routes/api.php';
});
$app->router->group([
'namespace' => 'App\Http\Controllers\Admin',
'prefix' => 'admin',
'middleware' => 'session'
], function ($router) {
require __DIR__ . '/../routes/admin.php';
});
return $app;
......@@ -35,3 +35,8 @@ function get_player($joins = true)
$_PLAYER = $player;
return $player;
}
include_once(dirname(__DIR__) . '/app/Helpers/Authentication.php');
include_once(dirname(__DIR__) . '/app/Helpers/Partials.php');
include_once(dirname(__DIR__) . '/app/Helpers/RandomStr.php');
include_once(dirname(__DIR__) . '/app/Helpers/TimeAgo.php');
\ No newline at end of file
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class AdminUsers extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('admin_users', function (Blueprint $table) {
$table->increments('id');
$table->integer('admin_user_group_id')->unsigned();
$table->string('full_name');
$table->string('email');
$table->string('password');
$table->timestamps();
$table->softDeletes(); // this means that we can keep ahold of old users but prevent access
$table->unique('email');
$table->index('admin_user_group_id');
});
Schema::create('admin_user_groups', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});
Schema::create('admin_password_reset_tokens', function (Blueprint $table) {
$table->increments('id');
$table->integer('admin_user_id');
$table->string('token');
$table->timestamps();
});
foreach (['admin'] as $_user_group) {
$user_group = new \App\AdminUserGroup();
$user_group->name = $_user_group;
$user_group->save();
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('admin_users');
Schema::dropIfExists('admin_user_groups');
Schema::dropIfExists('admin_password_reset_tokens');
}
}
const gulp = require('gulp'),
sass = require('gulp-sass'),
babel = require('gulp-babel'),
uglify = require('gulp-uglify'),
concat = require('gulp-concat');
let paths = {
scss: ['resources/assets/scss'],
js: ['resources/assets/js'],
dist_css: 'public/assets/css',
dist_js: 'public/assets/js',
};
// default tasks for gulp - generally this should be watch
gulp.task('default', ['sass', 'js']);
gulp.task('watch', ['default', 'sass:watch', 'js:watch']);
gulp.task('sass', () => {
gulp.src(paths.scss + '/admin.scss')
.pipe(sass({
outputStyle: 'compressed'
}).on('error', sass.logError))
.pipe(gulp.dest(paths.dist_css));
});
gulp.task('sass:watch', () => {
gulp.watch([paths.scss + '/*.scss', paths.scss + '/*/*.scss'], ['sass']);
});
gulp.task('js:watch', () => {
gulp.watch([
paths.js + '/admin.js'
], ['js']);
});
gulp.task('js', () => {
gulp.src([
paths.js + '/admin.js'
])
.pipe(babel({
'presets': ['es2015']
}))