session->get('active_workspace_id'); $userId = (int) $this->session->get('user_id'); $rules = [ 'type' => 'required|in_list[dashboard,chart]', 'resource_id' => 'required|integer', 'password' => 'permit_empty|max_length[200]', 'expires_at' => 'permit_empty|max_length[40]', ]; if (! $this->validate($rules)) { if ($this->request->isAJAX()) { return $this->response->setJSON(['success' => false, 'errors' => $this->validator->getErrors()])->setStatusCode(422); } return redirect()->back()->with('errors', $this->validator->getErrors()); } $type = (string) $this->request->getPost('type'); $resourceId = (int) $this->request->getPost('resource_id'); if ($type === 'dashboard') { $d = (new DashboardModel())->findForWorkspace($resourceId, $workspaceId); if (! $d) { return $this->ajaxOrRedirect(false, 'Dashboard not found.'); } } else { $c = (new ChartModel())->where('workspace_id', $workspaceId)->find($resourceId); if (! $c) { return $this->ajaxOrRedirect(false, 'Chart not found.'); } } $pwd = trim((string) $this->request->getPost('password')); $hash = $pwd !== '' ? password_hash($pwd, PASSWORD_DEFAULT) : null; $expRaw = trim((string) $this->request->getPost('expires_at')); $expiresAt = null; if ($expRaw !== '') { $ts = strtotime($expRaw); if ($ts === false) { return $this->ajaxOrRedirect(false, 'Invalid expiry date.'); } $expiresAt = date('Y-m-d H:i:s', $ts); } $model = new SharedLinkModel(); $token = SharedLinkModel::generateToken(); $id = (int) $model->insert([ 'token' => $token, 'type' => $type, 'resource_id' => $resourceId, 'workspace_id' => $workspaceId, 'password_hash' => $hash, 'expires_at' => $expiresAt, 'view_count' => 0, 'is_active' => 1, 'created_by' => $userId, ], true); $publicUrl = rtrim(base_url(), '/') . '/share/' . $token; AuditLogger::log( 'share.created', 'shared_link', $id, null, [ 'type' => $type, 'resource_id' => $resourceId, 'has_password' => $hash !== null, 'expires_at' => $expiresAt, ], $workspaceId, $userId ); if ($this->request->isAJAX()) { return $this->response->setJSON([ 'success' => true, 'id' => $id, 'token' => $token, 'url' => $publicUrl, 'view_count' => 0, 'csrf' => ['name' => csrf_token(), 'hash' => csrf_hash()], ]); } return redirect()->back()->with('success', 'Share link created: ' . $publicUrl); } public function revoke(int $id) { $workspaceId = (int) $this->session->get('active_workspace_id'); $model = new SharedLinkModel(); $row = $model->findOwned($id, $workspaceId); if (! $row) { return $this->ajaxOrRedirect(false, 'Link not found.'); } AuditLogger::log( 'share.revoked', 'shared_link', $id, ['is_active' => 1], ['is_active' => 0], $workspaceId, (int) $this->session->get('user_id') ); $model->update($id, ['is_active' => 0]); if ($this->request->isAJAX()) { return $this->response->setJSON([ 'success' => true, 'csrf' => ['name' => csrf_token(), 'hash' => csrf_hash()], ]); } return redirect()->back()->with('success', 'Share link revoked.'); } /** * Existing links for a resource (AJAX). */ public function listResource() { $workspaceId = (int) $this->session->get('active_workspace_id'); $type = (string) $this->request->getGet('type'); $resourceId = (int) $this->request->getGet('resource_id'); if (! in_array($type, ['dashboard', 'chart'], true) || $resourceId <= 0) { return $this->response->setJSON(['success' => false, 'message' => 'Invalid parameters.'])->setStatusCode(422); } if ($type === 'dashboard') { if (! (new DashboardModel())->findForWorkspace($resourceId, $workspaceId)) { return $this->response->setJSON(['success' => false])->setStatusCode(404); } } elseif (! (new ChartModel())->where('workspace_id', $workspaceId)->find($resourceId)) { return $this->response->setJSON(['success' => false])->setStatusCode(404); } $rows = (new SharedLinkModel())->forResource($type, $resourceId, $workspaceId); $out = []; foreach ($rows as $r) { $out[] = [ 'id' => (int) $r['id'], 'url' => rtrim(base_url(), '/') . '/share/' . $r['token'], 'view_count' => (int) ($r['view_count'] ?? 0), 'expires_at' => $r['expires_at'], 'has_password' => ! empty($r['password_hash']), ]; } return $this->response->setJSON(['success' => true, 'links' => $out]); } private function ajaxOrRedirect(bool $ok, string $message) { if ($this->request->isAJAX()) { return $this->response->setJSON(['success' => $ok, 'message' => $message])->setStatusCode($ok ? 200 : 404); } return redirect()->back()->with($ok ? 'success' : 'error', $message); } }