Commit 39b8bd98 authored by ZeMKI's avatar ZeMKI

New Layout + Optimization + Reset Password + Public Url Interface

* New layout and optimization based on tailwindcss.
* Reset password Function.
* New Public Url interface.
parent cb431e81
<?php
namespace App\Console\Commands;
use App\PublicInterviewUrl;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class PruneInterviewPublicUrl extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'pruneurls';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete not used urls older than 48h';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$urls = PublicInterviewUrl::where('created_at', '<=', Carbon::now()->subDays(2))->whereNull('submitted_at')->get();
$countUrls = $urls->count();
foreach($urls as $url)
{
$delete_shorturl = DB::table('art_urls')
->where('url','like', '%'.$url->id)
->delete();
$url->delete();
}
$this->info($countUrls." url(s) deleted.");
}
}
......@@ -16,6 +16,7 @@ class Kernel extends ConsoleKernel
Commands\CreateUserCommand::class,
Commands\DeleteClosedStudies::class,
Commands\DeleteUserCommand::class,
Commands\PruneInterviewPublicUrl::class,
];
/**
......
......@@ -3,10 +3,29 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\View\View;
class ResetPasswordController extends Controller
{
/**
* Get the password reset validation rules.
* @return array
*/
protected function rules()
{
return [
'token' => 'required',
'password' => 'required|confirmed|min:6',
];
}
/*
|--------------------------------------------------------------------------
| Password Reset Controller
......@@ -17,23 +36,63 @@ class ResetPasswordController extends Controller
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Display the password reset view for the given token.
* If no token is present, display the link request form.
* @param Request $request
* @param string|null $token
* @return Factory|View
*/
public function showResetForm(Request $request, $token = null)
{
return view('auth.passwords.resetPassword')->with(
['token' => $token, 'email' => $request->email]
);
}
/**
* Reset the given user's password.
* @param Request $request
* @return Factory|View
*/
public function reset(Request $request)
{
$request->validate($this->rules(), $this->validationErrorMessages());
// Validate the token
$tokenData = DB::table('password_resets')
->where('email', $request->email)->first();
if (!$tokenData)
{
return view('auth.passwords.email', ['message' => 'You don\'t have tokens!']);
} else if (!Hash::check($request->token, $tokenData->token))
{
return view('auth.passwords.email', ['message' => 'Token mismatch']);
}
//Delete the token
DB::table('password_resets')->where('email', $request->email)
->delete();
$user = User::where('email', '=', $request->email)->first();
$this->setUserPassword($user, $request->password);
$user->setRememberToken(Str::random(60));
$user->save();
return view('auth.login', ['message' => 'Password Reset Successfully done.']);
}
}
......@@ -2,9 +2,15 @@
namespace App\Http\Controllers\Auth;
use App\User;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Date;
use Illuminate\View\View;
class VerificationController extends Controller
{
......@@ -35,8 +41,50 @@ class VerificationController extends Controller
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
$this->middleware('auth')->except('showresetpassword','newpassword');
$this->middleware('signed')->only('verify')->except('showresetpassword','newpassword');
$this->middleware('throttle:6,1')->only('verify', 'resend')->except('showresetpassword','newpassword');
}
/**
* @param Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showresetpassword(Request $request)
{
if ($request->input('token') === '') {
return view('errors.resetpassword');
}
$user = User::where('password_token', '=', $request->input('token'))->first();
if (!$user) {
return view('errors.resetpassword');
}
$data['user'] = $user;
return view('auth.passwords.verify', $data);
}
/**
* @param Request $request
* @return Factory|RedirectResponse|Redirector|View
*/
public function newpassword(Request $request)
{
if ($request->input('token') === '') {
$data['error'] = 'wrong request, contact the administrator.';
$data['user'] = '';
return view('errors.resetpassword');
}
$user = User::where('password_token', '=', $request->input('token'))->first();
if (!$user) {
$data['error'] = 'Something went wrong, please contact the administrator.';
return view('errors.resetpassword');
}
$user->password_token = null;
$user->password = bcrypt($request->input('password'));
$user->email_verified_at = Date::now();
$user->save();
return redirect('/');
}
}
......@@ -5,24 +5,47 @@ namespace App\Http\Controllers;
use App\PublicInterviewUrl;
use ArieTimmerman\Laravel\URLShortener\URLShortener;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class PublicInterviewUrlController extends Controller
{
/**
* Store an url and create a shortened url
* @return \Illuminate\Http\JsonResponse
*/
public function store()
{
$uuid = Str::uuid();
$PublicInterviewUrl = new PublicInterviewUrl();
$PublicInterviewUrl->id = $uuid;
$PublicInterviewUrl->study_id = request()->study;
$PublicInterviewUrl->created_at = Carbon::now()->toDateTimeString('minutes');
$PublicInterviewUrl->save();
$url = (string)URLShortener::shorten(url('/interviews/new?study='.request()->study.'&t='.$uuid));
$PublicInterviewUrl->short_url_id = Carbon::now()->toDateTimeString('minutes');
return response()->json(['message' => 'Url Created!', 'url' => $url], 200);
if (request()->has('study'))
{
$uuid = Str::uuid();
$PublicInterviewUrl = new PublicInterviewUrl();
$PublicInterviewUrl->id = $uuid;
$PublicInterviewUrl->study_id = request()->study;
$PublicInterviewUrl->created_at = Carbon::now()->toDateTimeString('minutes');
$PublicInterviewUrl->save();
$url = (string)URLShortener::shorten(url('/interviews/new?study=' . request()->study . '&t=' . $uuid));
$PublicInterviewUrl->short_url_id = Carbon::now()->toDateTimeString('minutes');
return response()->json(['message' => 'Url Created! Refresh the page to access the interview list.', 'url' => $url], 200);
} else
{
return response()->json(['message' => 'Please set a study.'], 200);
}
}
/**
* Remove the specified resource from storage.
* @return Response
*/
public function destroy()
{
$url = PublicInterviewUrl::where('id', '=' ,request()->id)->first();
$url->delete();
$delete_shorturl = DB::table('art_urls')
->where('id','=', request()->url_id)
->delete();
return response('Url deleted', 200);
}
}
......@@ -13,9 +13,23 @@ class StudyInterviewController extends Controller
{
if (!auth()->user()->can('read-studies', $study->id)) abort(403, "You are not authorized to see this content.");
$data['interviews'] = $study->interviews()->get();
$data['publicInterviews'] = collect(DB::select('select study_interview_public_url.id,url,first_opened_at,submitted_at from art_urls inner join study_interview_public_url on art_urls.url like CONCAT(\'%\',study_interview_public_url.id,\'%\') where study_id = :study', ['study' => $study->id]));
$data['study'] = $study;
$data['publicInterviews'] = collect(DB::select('
select study_interview_public_url.id,
art_urls.id as url_id,
url,first_opened_at,
submitted_at,
code
from art_urls
inner join study_interview_public_url
on art_urls.url like CONCAT(\'%\',study_interview_public_url.id,\'%\')
where study_id = :study', ['study' => $study->id]));
foreach ($data['publicInterviews'] as $publicInterview)
{
$publicInterview->url = url('','short')."=".$publicInterview->code;
}
$data['study'] = $study;
foreach ($data['interviews'] as $interview)
{
$interview['author'] = User::where('id', $interview['author'])->first()->email ?? $interview['author'];
......
......@@ -267,18 +267,6 @@ class UserController extends Controller
}
public function showresetpassword(Request $request)
{
if ($request->input('token') === '') {
return view('errors.resetpassword');
}
$user = User::where('password_token', '=', $request->input('token'))->first();
if (!$user) {
return view('errors.resetpassword');
}
$data['user'] = $user;
return view('auth.passwords.reset', $data);
}
/**
* @param User $user
......@@ -293,26 +281,4 @@ class UserController extends Controller
return response('Password is resetted for ' . $user->email . ' and an email was sent.');
}
/**
* @param Request $request
* @return Factory|RedirectResponse|Redirector|View
*/
public function newpassword(Request $request)
{
if ($request->input('token') === '') {
$data['error'] = 'wrong request, contact the administrator.';
$data['user'] = '';
return view('errors.resetpassword');
}
$user = User::where('password_token', '=', $request->input('token'))->first();
if (!$user) {
$data['error'] = 'Something went wrong, please contact the administrator.';
return view('errors.resetpassword');
}
$user->password_token = null;
$user->password = bcrypt($request->input('password'));
$user->email_verified_at = Date::now();
$user->save();
return redirect('/');
}
}
......@@ -73,6 +73,12 @@ class Study extends LaratrustTeam
);
}
public function publicUrls()
{
return $this->hasMany(PublicInterviewUrl::class);
}
public function sortings()
{
return $this->belongsToMany(Sorting::class, 'study_sortings')->withPivot('details')->withTimestamps();
......
......@@ -6,7 +6,7 @@ return [
'characterset' => env('URLSHORTENER_CHARACTERSET', "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"),
// The minimum character length
'length_min' => env('URLSHORTENER_LENGTH_MIN', 4),
'length_min' => env('URLSHORTENER_LENGTH_MIN', 5),
// Optionally, an URL prefix. May be left empty. However, this would require registering the `routes` of this package after all your other routes
'url_prefix' => env('URLSHORTENER_URL_PREFIX', ""),
......
......@@ -218,21 +218,30 @@ window.app = new Vue({
},
methods: {
copyInterviewUrlToClipboard() {
let testingCodeToCopy = this.interview.url;
testingCodeToCopy.setAttribute('type', 'text') // 不是 hidden 才能複製
testingCodeToCopy.select();
try {
var successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful';
alert('Testing code was copied ' + msg);
} catch (err) {
alert('Oops, unable to copy');
if (this.interview.url != "") {
let testingCodeToCopy = document.querySelector('#publicUrl')
testingCodeToCopy.setAttribute('type', 'text') // 不是 hidden 才能複製
testingCodeToCopy.select()
let self = this;
try {
var successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful';
self.$buefy.snackbar.open("Url copied to Clipboard.");
} catch (err) {
self.$buefy.snackbar.open("Unable to copy.");
}
/* unselect the range */
window.getSelection().removeAllRanges()
} else {
this.$buefy.snackbar.open("The url is empty.");
}
/* unselect the range */
testingCodeToCopy.setAttribute('type', 'hidden')
window.getSelection().removeAllRanges()
},
toggleModal(id = "") {
const body = document.querySelector('body')
......@@ -240,7 +249,12 @@ window.app = new Vue({
modal.classList.toggle('opacity-0')
modal.classList.toggle('pointer-events-none')
body.classList.toggle('modal-active')
this.interview.study = id;
if (id != "" && id != this.interview.study) {
this.interview.url = "";
this.$forceUpdate();
}
if (id != "") this.interview.study = id;
},
createPublicUrl() {
let self = this;
......@@ -251,7 +265,7 @@ window.app = new Vue({
self.$buefy.snackbar.open(response.data.message);
self.interview.url = response.data.url;
this.$forceUpdate();
self.$forceUpdate();
}).catch(function (error) {
......
......@@ -11,9 +11,6 @@
:sort-icon-size="sortIconSize"
>
<template slot-scope="props">
<b-table-column field="id" label="ID" width="40" numeric>
{{ props.row.id }}
</b-table-column>
<b-table-column field="url" :label="trans('Url')" sortable>
{{ props.row.url }}
......@@ -32,7 +29,7 @@
</b-table-column>
<b-table-column field="buttons" label="Actions" centered width="400">
<span><a href="#" @click="confirmdelete(props.row.id)"
<span><a href="#" @click="confirmdelete(props.row.id,props.row.url_id)"
class="bg-red-300 hover:bg-red-400 text-black-800 font-bold py-2 px-4 rounded inline-flex items-center">
<b-icon
class="fill-current w-4 h-4 mr-2"
......@@ -80,25 +77,30 @@
return moment(String(date)).format(this.getLocaleDateString());
},
confirmdelete: function (id) {
confirmdelete: function (id, url_id) {
let confirmDelete = this.$buefy.dialog.confirm(
{
title: 'Confirm Delete',
message: '<div class="bg-red-600 p-2 text-white text-center">You re about to delete this interview.<br><span class="has-text-weight-bold">Continue?</span></div>',
message: '<div class="bg-red-600 p-2 text-white text-center">You re about to delete this url.<br><span class="has-text-weight-bold">Continue?</span></div>',
cancelText: 'Cancel',
confirmText: 'YES delete Interview',
confirmText: 'YES delete URL',
hasIcon: true,
type: 'is-danger',
onConfirm: () => this.deleteurl(id)
onConfirm: () => this.deleteurl(id, url_id)
}
);
},
deleteurl: function (id) {
deleteurl: function (id, url_id) {
this.loading = true;
this.message = "";
let self = this;
axios.delete(window.location.origin + this.productionUrl + '/interview/' + id, {data: id})
axios.delete(window.location.origin + this.productionUrl + '/interview/publicurl/delete', {
data: {
id: id,
url_id: url_id
}
})
.then(response => {
setTimeout(function () {
self.loading = false;
......
This diff is collapsed.
......@@ -7,6 +7,7 @@
@import 'bulma';
@import 'bulma-slider.sass';
@import '../../node_modules/vue-select/dist/vue-select.css';
@import "~tailwindcss/base";
@import "~tailwindcss/components";
......@@ -18,9 +19,7 @@
height: calc(100vh - 56px);
}
.vs__dropdown-option{
padding: 3px 3px;
}
.vue-select-fix-width{
overflow: hidden;
......@@ -207,35 +206,6 @@ transform: scaleY(1.0);
}
}
.bg-img {
background-image: url("../images/login_background.jpg");
background-position: center center;
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
background-color: #999;
height: 100%;
width: 100%;
-webkit-filter: blur(5px);
-moz-filter: blur(5px);
-o-filter: blur(5px);
-ms-filter: blur(5px);
filter: blur(5px);
}
.menu-item{
color: #046cbe;
display: block;
line-height: 1.5;
font-size:16px;
}
.has-text-blue{
color: #046cbe;
}
......
......@@ -31,11 +31,11 @@
</head>
<body>
<div id="app">
<div id="app" class="bg-blue-900">