From cc0fe080cffa43672251b933e784d0f5668e6154 Mon Sep 17 00:00:00 2001 From: James Collins Date: Mon, 1 May 2023 19:04:08 +1000 Subject: [PATCH] analytics backend update --- app/Conductors/AnalyticsConductor.php | 76 ++++++++++ .../Controllers/Api/AnalyticsController.php | 133 ++++++++++++++++++ app/Http/Middleware/LogRequest.php | 4 +- app/Http/Requests/AnalyticsRequest.php | 35 +++++ app/Models/Analytics.php | 27 ++++ ...23_05_01_045630_update_analytics_table.php | 76 ++++++++++ routes/api.php | 3 + 7 files changed, 352 insertions(+), 2 deletions(-) create mode 100644 app/Conductors/AnalyticsConductor.php create mode 100644 app/Http/Controllers/Api/AnalyticsController.php create mode 100644 app/Http/Requests/AnalyticsRequest.php create mode 100644 database/migrations/2023_05_01_045630_update_analytics_table.php diff --git a/app/Conductors/AnalyticsConductor.php b/app/Conductors/AnalyticsConductor.php new file mode 100644 index 0000000..cd257da --- /dev/null +++ b/app/Conductors/AnalyticsConductor.php @@ -0,0 +1,76 @@ +user(); + return ($user !== null && $user->hasPermission('admin/analytics') === true); + } + + /** + * Return if the current model is creatable. + * + * @return boolean Allow creating model. + */ + public static function creatable() + { + return true; + } + + /** + * Return if the current model is updatable. + * + * @param Model $model The model. + * @return boolean Allow updating model. + */ + public static function updatable(Model $model) + { + $user = auth()->user(); + return ($user !== null && $user->hasPermission('admin/analytics') === true); + } + + /** + * Return if the current model is destroyable. + * + * @param Model $model The model. + * @return boolean Allow deleting model. + */ + public static function destroyable(Model $model) + { + $user = auth()->user(); + return ($user !== null && $user->hasPermission('admin/analytics') === true); + } +} diff --git a/app/Http/Controllers/Api/AnalyticsController.php b/app/Http/Controllers/Api/AnalyticsController.php new file mode 100644 index 0000000..8167905 --- /dev/null +++ b/app/Http/Controllers/Api/AnalyticsController.php @@ -0,0 +1,133 @@ +middleware('auth:sanctum') + ->only([ + 'index', + 'update', + 'delete' + ]); + } + + /** + * Display a listing of the resource. + * + * @param \Illuminate\Http\Request $request The endpoint request. + * @return \Illuminate\Http\Response + */ + public function index(Request $request) + { + list($collection, $total) = AnalyticsConductor::request($request); + + return $this->respondAsResource( + $collection, + ['isCollection' => true, + 'appendData' => ['total' => $total] + ] + ); + } + + /** + * Display the specified resource. + * + * @param \Illuminate\Http\Request $request The endpoint request. + * @param \App\Models\Analytics $analytics The analyics model. + * @return \Illuminate\Http\Response + */ + public function show(Request $request, Analytics $analytics) + { + if (AnalyticsConductor::viewable($analytics) === true) { + return $this->respondAsResource(AnalyticsConductor::model($request, $analytics)); + } + + return $this->respondForbidden(); + } + + /** + * Store a newly created resource in storage. + * + * @param \App\Http\Requests\AnalyticsRequest $request The user request. + * @return \Illuminate\Http\Response + */ + public function store(AnalyticsRequest $request) + { + if (AnalyticsConductor::creatable() === true) { + $analytics = null; + $user = $request->user(); + + $data = [ + 'type' => $request->input('type'), + 'attribute' => $request->input('attribute', ''), + 'useragent' => $request->userAgent(), + 'ip' => $request->ip() + ]; + + if ($user !== null && $user->hasPermission('admin/analytics') === true && $request->has('session') === true) { + $data['session'] = $request->input('session'); + $analytics = Analytics::create($data); + } else { + $analytics = Analytics::createWithSession($data); + } + + return $this->respondAsResource( + AnalyticsConductor::model($request, $analytics), + ['respondCode' => HttpResponseCodes::HTTP_CREATED] + ); + } else { + return $this->respondForbidden(); + }//end if + } + + /** + * Update the specified resource in storage. + * + * @param \App\Http\Requests\AnalyticsRequest $request The analytics update request. + * @param \App\Models\Analytics $analytics The specified analytics. + * @return \Illuminate\Http\Response + */ + public function update(AnalyticsRequest $request, Analytics $analytics) + { + if (AnalyticsConductor::updatable($analytics) === true) { + $analytics->update($request->all()); + return $this->respondAsResource(AnalyticsConductor::model($request, $analytics)); + } + + return $this->respondForbidden(); + } + + /** + * Remove the specified resource from storage. + * + * @param \App\Models\Analytics $analytics The specified analytics. + * @return \Illuminate\Http\Response + */ + public function destroy(Analytics $analytics) + { + if (AnalyticsConductor::destroyable($analytics) === true) { + $analytics->delete(); + return $this->respondNoContent(); + } else { + return $this->respondForbidden(); + } + } +} diff --git a/app/Http/Middleware/LogRequest.php b/app/Http/Middleware/LogRequest.php index 69927af..1ecc38f 100644 --- a/app/Http/Middleware/LogRequest.php +++ b/app/Http/Middleware/LogRequest.php @@ -21,8 +21,8 @@ class LogRequest $response = $next($request); try { - Analytics::create([ - 'type' => 'pageview', + Analytics::createWithSession([ + 'type' => 'apirequest', 'attribute' => $request->path(), 'useragent' => $request->userAgent(), 'ip' => $request->ip(), diff --git a/app/Http/Requests/AnalyticsRequest.php b/app/Http/Requests/AnalyticsRequest.php new file mode 100644 index 0000000..bc90a99 --- /dev/null +++ b/app/Http/Requests/AnalyticsRequest.php @@ -0,0 +1,35 @@ + + */ + public function postRules() + { + return [ + 'type' => 'required|string', + ]; + } + + /** + * Get the validation rules that apply to PUT request. + * + * @return array + */ + public function putRules() + { + return [ + 'type' => 'string', + 'useragent' => 'string', + 'ip' => 'ipv4|ipv6', + 'session' => 'number', + ]; + } +} diff --git a/app/Models/Analytics.php b/app/Models/Analytics.php index 04eb5e1..f5c400f 100644 --- a/app/Models/Analytics.php +++ b/app/Models/Analytics.php @@ -15,4 +15,31 @@ class Analytics extends Model * @var array */ protected $guarded = []; + + + /** + * Create a new row in the analytics table with the given attributes, + * automatically assigning a session value based on previous rows. + * + * @param array $attributes Model attributes. + * @return static + */ + public static function createWithSession(array $attributes) + { + $previousRow = self::where('useragent', $attributes['useragent']) + ->where('ip', $attributes['ip']) + ->where('created_at', '>=', now()->subMinutes(30)) + ->whereNotNull('session') + ->orderBy('created_at', 'desc') + ->first(); + + if ($previousRow !== null) { + $attributes['session'] = $previousRow->session; + } else { + $lastSession = self::max('session'); + $attributes['session'] = ($lastSession + 1); + } + + return static::create($attributes); + } } diff --git a/database/migrations/2023_05_01_045630_update_analytics_table.php b/database/migrations/2023_05_01_045630_update_analytics_table.php new file mode 100644 index 0000000..bf80fb9 --- /dev/null +++ b/database/migrations/2023_05_01_045630_update_analytics_table.php @@ -0,0 +1,76 @@ +bigInteger('session')->nullable(false); + $table->string('attribute')->default('')->change(); + }); + + DB::table('analytics') + ->where('type', 'pageview') + ->update(['type' => 'apirequest']); + + $rows = DB::table('analytics') + ->whereNull('session') + ->orderBy('created_at', 'asc') + ->get(); + + // Loop through the rows and update `session` based on the logic you described + $session = 1; + foreach ($rows as $row) { + // Check if this is the first row + if ($row->created_at === $rows->first()->created_at) { + DB::table('analytics') + ->where('id', $row->id) + ->update(['session' => $session]); + } else { + // Look for a previous row with the same useragent and ip within the last 30 minutes + $previousRow = DB::table('analytics') + ->where('useragent', $row->useragent) + ->where('ip', $row->ip) + ->where('created_at', '>=', date('Y-m-d H:i:s', strtotime('-30 minutes', strtotime($row->created_at)))) + ->whereNotNull('session') + ->orderBy('created_at', 'desc') + ->first(); + + if ($previousRow) { + // If a previous row is found, set the session to the same value + DB::table('analytics') + ->where('id', $row->id) + ->update(['session' => $previousRow->session]); + } else { + // If no previous row is found, increment the session value + $session++; + DB::table('analytics') + ->where('id', $row->id) + ->update(['session' => $session]); + } + } + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('analytics', function (Blueprint $table) { + $table->dropColumn('session'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 09dff83..c5ec119 100644 --- a/routes/api.php +++ b/routes/api.php @@ -25,6 +25,9 @@ use App\Http\Controllers\Api\UserController; Route::post('/login', [AuthController::class, 'login'])->name('login'); Route::post('/register', [UserController::class, 'register']); +Route::get('/analytics', [AnalyticsController::class, 'index']); +Route::post('/analytics', [AnalyticsController::class, 'store']); + Route::apiResource('users', UserController::class); Route::post('/users/forgotUsername', [UserController::class, 'forgotUsername']); Route::post('/users/forgotPassword', [UserController::class, 'forgotPassword']);