Commit cb431e81 authored by ZeMKI's avatar ZeMKI

New Function Public Url

* installed url shortener.
* new migrations.
* new middleware to check if the token is correct.
* new home layout.
* css optimization.
parent 5b2413b1
......@@ -6,6 +6,7 @@
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" packagePrefix="Tests\" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/public" />
<excludeFolder url="file://$MODULE_DIR$/vendor/arietimmerman/laravel-url-shortener" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-debugbar" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-dompdf" />
<excludeFolder url="file://$MODULE_DIR$/vendor/barryvdh/laravel-translation-manager" />
......@@ -113,6 +114,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/sensiolabs/security-checker" />
<excludeFolder url="file://$MODULE_DIR$/vendor/slam/php-cs-fixer-extensions" />
<excludeFolder url="file://$MODULE_DIR$/vendor/slevomat/coding-standard" />
<excludeFolder url="file://$MODULE_DIR$/vendor/spatie/laravel-webhook-server" />
<excludeFolder url="file://$MODULE_DIR$/vendor/squizlabs/php_codesniffer" />
<excludeFolder url="file://$MODULE_DIR$/vendor/stichoza/google-translate-php" />
<excludeFolder url="file://$MODULE_DIR$/vendor/swiftmailer/swiftmailer" />
......@@ -151,6 +153,7 @@
<excludeFolder url="file://$MODULE_DIR$/vendor/tijsverkoyen/css-to-inline-styles" />
<excludeFolder url="file://$MODULE_DIR$/vendor/vlucas/phpdotenv" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webmozart/assert" />
<excludeFolder url="file://$MODULE_DIR$/vendor/webpatser/laravel-uuid" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
......
......@@ -157,6 +157,9 @@
<path value="$PROJECT_DIR$/vendor/jasonmccreary/laravel-test-assertions" />
<path value="$PROJECT_DIR$/vendor/parsedown/laravel" />
<path value="$PROJECT_DIR$/vendor/erusev/parsedown" />
<path value="$PROJECT_DIR$/vendor/webpatser/laravel-uuid" />
<path value="$PROJECT_DIR$/vendor/arietimmerman/laravel-url-shortener" />
<path value="$PROJECT_DIR$/vendor/spatie/laravel-webhook-server" />
</include_path>
</component>
<component name="PhpProjectSharedConfiguration" php_language_level="7.2" />
......
......@@ -181,4 +181,6 @@ $app/Http/Middleware/Authenticate.php,f/7/f7ec0e145842212dd9c0763d10bb0aaa68d32
<
app/Role.php,5/0/506466bcf9d9bc1f32f19b46aa47d9cf4fbbe185
C
app/Http/Kernel.php,4/0/405f423e61f666bcedb754ae8befdce9e746176e
\ No newline at end of file
app/Http/Kernel.php,4/0/405f423e61f666bcedb754ae8befdce9e746176e
K
app/Policies/UserPolicy.php,7/2/72a6f8426b50f2d6d3c0bdc2da818be7479c8989
\ No newline at end of file
......
......@@ -37,7 +37,7 @@ class InterviewController extends Controller
$data['study'] = $interview->study;
$data['questions'] = $interview->study->questions;
$data['sorting'] = $interview->study->sortings[0];
$data['author'] = User::where('id', $interview->author)->first();
$data['author'] = User::where('id', $interview->author)->first() ?? $interview->author;
Answer::assignAnswersToQuestion($interview, $data);
Sorting::getSortingInfo($interview->study, $data);
Interview::getSortingImages($interview, $data);
......@@ -53,7 +53,8 @@ class InterviewController extends Controller
*/
public function create(Request $request)
{
$this->authorize([Interview::class, $request->input('study')]);
if(auth()->check()) $this->authorize([Interview::class, $request->input('study')]);
/** Extract method FORMATQUESTIONSANSWERS */
$this->FormatQuestionsAndAnswers($request, $returnQuestions);
$data['questions'] = $returnQuestions;
......@@ -121,9 +122,12 @@ class InterviewController extends Controller
public function store(Request $request)
{
$study = Study::where('id', '=', $request->input('study'))->first();
$author = auth()->check() ? Auth::user()->id : 'From public url.';
$interview = new Interview();
$interview->study_id = $study->id;
$interview->author = Auth::user()->id;
$interview->author = $author;
$interview->interviewed = $request->input('interviewed');
$interview->start = $request->input('time_start');
$interview->end = $request->input('time_end');
......@@ -186,4 +190,10 @@ class InterviewController extends Controller
if (!auth()->user()->can('read-studies', $interview->study_id)) abort(403, "You are not authorized to download these data.");
return (new InterviewTokenExport($interview->id))->download($interview->interviewed . ' tokens.xlsx');
}
public function done()
{
return view('interview.done');
}
}
<?php
namespace App\Http\Controllers;
use App\PublicInterviewUrl;
use ArieTimmerman\Laravel\URLShortener\URLShortener;
use Carbon\Carbon;
use Illuminate\Support\Str;
class PublicInterviewUrlController extends Controller
{
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);
}
}
......@@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use App\Exports\AllInterviewTokenExport;
use App\Study;
use App\User;
use Illuminate\Support\Facades\DB;
class StudyInterviewController extends Controller
{
......@@ -12,9 +13,12 @@ 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;
foreach ($data['interviews'] as $interview) {
$interview['author'] = User::where('id', $interview['author'])->first()->email;
foreach ($data['interviews'] as $interview)
{
$interview['author'] = User::where('id', $interview['author'])->first()->email ?? $interview['author'];
}
return view('interview.index', $data);
}
......
......@@ -62,5 +62,6 @@ class Kernel extends HttpKernel
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'interview' => \App\Http\Middleware\PublicInterviewTokenCheck::class,
];
}
<?php
namespace App\Http\Middleware;
use App\PublicInterviewUrl;
use Carbon\Carbon;
use Closure;
use Spatie\WebhookServer\Exceptions\CouldNotCallWebhook;
use Spatie\WebhookServer\WebhookCall;
class PublicInterviewTokenCheck
{
/**
* @param $request
* @param Closure $next
* @return mixed
* @throws CouldNotCallWebhook
*/
public function handle($request, Closure $next)
{
if (auth()->check())
{
return $next($request);
} else if (!$request->has("t") && request()->isMethod('get'))
{
abort(403);
}
$uuid = request()->t;
$validToken = PublicInterviewUrl::isValid($uuid);
if ($validToken)
{
if (is_null($validToken->first_opened_at))
{
$validToken->first_opened_at = Carbon::now()->toDateTimeString('minutes');
$validToken->save();
}
if(request()->isMethod('post'))
{
$validToken->submitted_at = Carbon::now()->toDateTimeString('minutes');
$validToken->save();
}
return $next($request);
} else
{
WebhookCall::create()
->url('https://chat.zemki.uni-bremen.de/hooks/Jj3dDY2KzSFDS2kxZ/SvbmjdswXTASAXxC2GfgfTpFooK5Eo4kFBGPyDRrtsWmgED3')
->payload(['text' => 'Someone tried to do an interview with a wrong Token on Mesort from ' . request()->ip()])
->useSecret('Jj3dDY2KzSFDS2kxZ/SvbmjdswXTASAXxC2GfgfTpFooK5Eo4kFBGPyDRrtsWmgED3')
->dispatch();
abort(403, "Token not valid, contact your reference person.");
}
}
}
......@@ -2,11 +2,13 @@
namespace App\Policies;
use Auth;
use App\User;
use App\Study;
use App\Interview;
use App\PublicInterviewUrl;
use App\Study;
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Support\Facades\Log;
class InterviewPolicy
{
......@@ -27,40 +29,36 @@ class InterviewPolicy
/**
* Determine whether the user can view the interview.
*
* @param User $user
* @param User $user
* @param Interview $interview
* @return mixed
*/
public function view(User $user, Interview $interview)
{
if (! $user->can('read-studies', Study::where('id', '=', $interview->study_id)->first()->id)) {
if (!$user->can('read-studies', Study::where('id', '=', $interview->study_id)->first()->id)) {
abort(403, 'You are not allowed to read this study');
}
return true;
}
/**
* Determine whether the user can create interviews.
*
* @param User $user
* @param User $user
* @param Study $study
* @return mixed
*/
public function create(User $user, $study)
{
if (! $user->can('create-interviews', Study::where('id', '=', $study)->first()->id)) {
if (!$user->can('create-interviews', Study::where('id', '=', $study)->first()->id)) {
abort(403, 'You are not allowed to create interviews for this study');
}
return true;
}
/**
* Determine whether the user can update the interview.
*
* @param User $user
* @param User $user
* @param Interview $interview
* @return void
*/
......@@ -70,8 +68,7 @@ class InterviewPolicy
/**
* Determine whether the user can delete the interview.
*
* @param User $user
* @param User $user
* @param Interview $interview
* @return void
*/
......@@ -81,8 +78,7 @@ class InterviewPolicy
/**
* Determine whether the user can restore the interview.
*
* @param User $user
* @param User $user
* @param Interview $interview
* @return void
*/
......@@ -92,8 +88,7 @@ class InterviewPolicy
/**
* Determine whether the user can permanently delete the interview.
*
* @param User $user
* @param User $user
* @param Interview $interview
* @return void
*/
......@@ -101,11 +96,11 @@ class InterviewPolicy
{
}
public function exportall(User $user, $study){
if ( $user->can('read-studies', Study::where('id', '=', $study)->first()->id)) {
public function exportall(User $user, $study)
{
if ($user->can('read-studies', Study::where('id', '=', $study)->first()->id)) {
abort(403, 'You are not allowed to read this study');
}
return true;
}
}
......@@ -2,7 +2,9 @@
namespace App\Providers;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
......@@ -15,9 +17,16 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
Schema::defaultStringLength(191);
\Illuminate\Support\Facades\View::composer(['telescope::layout'], function ($view) {
$view->with('telescopeScriptVariables', ['path' => strtolower(env('APP_NAME', 'mesort')).'/telescope', 'timezone' => config('app.timezone'), 'recording' => ! cache('telescope:pause-recording')]);
});
if (App::environment('local')) {
// The environment is local
View::composer(['telescope::layout'], function ($view) {
$view->with('telescopeScriptVariables', ['path' => 'telescope', 'timezone' => config('app.timezone'), 'recording' => !cache('telescope:pause-recording')]);
});
}else{
View::composer(['telescope::layout'], function ($view) {
$view->with('telescopeScriptVariables', ['path' => 'mesort/telescope', 'timezone' => config('app.timezone'), 'recording' => !cache('telescope:pause-recording')]);
});
}
}
/**
......
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class PublicInterviewUrl extends Model
{
/**
* @var string
*/
protected $table = 'study_interview_public_url';
public $timestamps = false;
public static function isValid($uuid)
{
return PublicInterviewUrl::where('id', '=', $uuid)->whereNull('submitted_at')->first();
}
}
This diff is collapsed.
<?php
return [
// The characters used to generate an unique URL
'characterset' => env('URLSHORTENER_CHARACTERSET', "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"),
// The minimum character length
'length_min' => env('URLSHORTENER_LENGTH_MIN', 4),
// 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', ""),
// Use a prefix for the generated URL code
'url_prefix_code' => env('URLSHORTENER_URL_PREFIX_CODE', "short="),
// Allows disabling the resource endpoint
'route_resource_enabled' => env('URLSHORTENER_ROUTE_RESOURCE_ENABLED', true)
];
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateStudyInterviewPublicUrlTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('study_interview_public_url', function (Blueprint $table) {
$table->uuid('id')->index();
$table->integer('study_id')->nullable()->unsigned()->references('id')->on('studies')->onDelete('cascade');
$table->datetime('created_at');
$table->datetime('first_opened_at')->nullable();
$table->datetime('submitted_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('study_interview_public_url');
}
}
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
{
"/app.js": "/app.js?id=cb082a14c0f0bb5764c9",
"/app.css": "/app.css?id=7c1ff2a14db6cf79b2c3",
"/app-dark.css": "/app-dark.css?id=3499432c9dbc93b0f541"
}
\ No newline at end of file
"/app.js": "/app.js?id=f2e12e6d70f933613e34",
"/app.css": "/app.css?id=326b997a579e007d2102",
"/app-dark.css": "/app-dark.css?id=5ebb5e33b7c425d6317c"
}
......@@ -30,7 +30,7 @@ if (process.env.MIX_ENV_MODE === 'production') {
Vue.config.debug = false;
Vue.config.silent = true;
}else{
} else {
Vue.config.devtools = true;
Vue.config.debug = true;
Vue.config.silent = false;
......@@ -38,7 +38,7 @@ if (process.env.MIX_ENV_MODE === 'production') {
}
Vue.prototype.trans = (key) => {
return _.isUndefined(window.trans[key])? key : window.trans[key];
return _.isUndefined(window.trans[key]) ? key : window.trans[key];
};
/**
......@@ -55,16 +55,14 @@ Vue.component('new-interview', require('./components/newinterview.vue').default)
Vue.component('sorting', require('./components/sorting.vue').default);
Vue.component('userpart', require('./components/userpart.vue').default);
Vue.component('interview-list', require('./components/interviewlist.vue').default);
Vue.component('url-list', require('./components/publicurllist').default);
Vue.component('new-token', require('./components/newtokenmodal.vue').default);
Vue.component('action-table', require('./components/actiontable.vue').default);
Vue.component('user-table', require('./components/usertable.vue').default);
var bus = new Vue();
// Assign globally functions for getCookies and setCookies in JS
Vue.mixin({
data() {
return {
......@@ -173,7 +171,9 @@ window.app = new Vue({
activeTab: 0
},
interview: {
interviewed: ""
interviewed: "",
study: "",
url: ""
},
registration: {
password: null,
......@@ -191,6 +191,7 @@ window.app = new Vue({
created() {
let self = this;
if (this.url == "") {
let self = this;
axios.post('users/notifications')
......@@ -216,6 +217,50 @@ 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');
}
/* unselect the range */
testingCodeToCopy.setAttribute('type', 'hidden')
window.getSelection().removeAllRanges()
},
toggleModal(id = "") {
const body = document.querySelector('body')
const modal = document.querySelector('.modal')
modal.classList.toggle('opacity-0')
modal.classList.toggle('pointer-events-none')
body.classList.toggle('modal-active')
this.interview.study = id;
},
createPublicUrl() {
let self = this;
axios.post('interview/publicurl/create', {study: this.interview.study})
.then(response => {
// if set to read set background different
self.$buefy.snackbar.open(response.data.message);
self.interview.url = response.data.url;
this.$forceUpdate();
}).catch(function (error) {
console.log(error);
self.$buefy.snackbar.open("There it was an error during the request - refresh page and try again");
});
},
checkPassword() {
this.registration.password_length = this.registration.password.length;
const special_chars = /[ !@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/;
......@@ -259,8 +304,6 @@ window.app = new Vue({
axios.post('users/notifications/update', {notification: id})
.then(response => {
// if set to read set background different
self.$buefy.snackbar.open("Notification set to read");
......
<template>
<section>
<b-table
:data="isEmpty ? [] : interviews"
bordered
......@@ -69,6 +68,7 @@
</section>
</template>
</b-table>
</section>
</template>
......@@ -116,7 +116,7 @@
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/' + id, {data: id})
.then(response => {
setTimeout(function () {
self.loading = false;
......
......@@ -97,7 +97,9 @@
<span class="absolute w-full overflow-hidden" style="bottom:10px;" id="classifiercontainer">
<img v-for="(image,index) in classifiers[0]"
v-show="(classifier == image.name)"
:src="image.dirname" class="w-10 h-10 inline-block ml-3 z-50" :id="'class'+index"
:src="image.dirname" class="w-10 h-10 inline-block ml-3 z-50"
:id="'class'+index"
:alt="image.dirname"
@click="setClassifier(index)">
</span>
......@@ -295,6 +297,13 @@
},
methods: {
getUrlVars: function () {
var vars = {};
var parts = window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function (m, key, value) {
vars[key] = value;
});
return vars;
},
removeSelectionFromAll: function () {
_.forEach(this.classifiers[0], function (value, key) {
let cl = document.getElementById('class' + key);
......@@ -328,7 +337,6 @@
}
},
createcircles: function () {
......@@ -454,7 +462,7 @@
removeElementsFromScreenshot: function () {
let setOpacityTo0 = document.getElementsByClassName("remove-from-screenshot");
let navbarHeight = document.getElementById("main-nav-interview").offsetHeight;
document.querySelector(".round-sorting" + this.circles).style.marginTop = navbarHeight+"px";
document.querySelector(".round-sorting" + this.circles).style.marginTop = navbarHeight + "px";
_.forEach(setOpacityTo0, function (o) {
o.style.opacity = "0.0";
......@@ -724,6 +732,8 @@
,
saveinterview: async function () {
let t = this.getUrlVars()["t"];
this.loading = true;
let self = this;
var screenshotsaved = false;
......@@ -733,7 +743,7 @@
await this.capture_screenshot().then(function () {
screenshotsaved = true;
});
}else{
} else {
screenshotsaved = true;
}