where('workspace_id', $workspaceId) ->where('user_id', (int) $this->session->get('user_id')) ->first() !== null; } public function workspace() { $workspaceId = (int) $this->session->get('active_workspace_id'); if (! $this->ensureWorkspaceMembership($workspaceId)) { return redirect()->to('/workspace')->with('error', 'Access denied.'); } $workspace = (new WorkspaceModel())->find($workspaceId); if (! $workspace) { return redirect()->to('/workspace')->with('error', 'Workspace not found.'); } $defaultTheme = (new SettingsModel())->getValue($workspaceId, 'default_theme', 'system') ?? 'system'; return view('settings/workspace', [ 'title' => 'Workspace preferences | Chart-Board', 'workspace' => $workspace, 'default_theme' => $defaultTheme, ]); } public function saveWorkspace() { $workspaceId = (int) $this->session->get('active_workspace_id'); if (! $this->ensureWorkspaceMembership($workspaceId)) { return redirect()->to('/workspace')->with('error', 'Access denied.'); } $workspaceModel = new WorkspaceModel(); $workspace = $workspaceModel->find($workspaceId); if (! $workspace) { return redirect()->to('/workspace')->with('error', 'Workspace not found.'); } $rules = [ 'name' => 'required|min_length[3]|max_length[150]', 'description' => 'permit_empty|max_length[1000]', 'timezone' => 'required|max_length[80]', 'default_refresh' => 'required|in_list[60,300,900,3600]', 'is_active' => 'required|in_list[0,1]', 'default_theme' => 'required|in_list[light,dark,system]', 'logo' => 'permit_empty|uploaded[logo]|max_size[logo,512]|is_image[logo]|mime_in[logo,image/jpg,image/jpeg,image/png,image/webp,image/svg+xml]', ]; $hasLogo = $this->request->getFile('logo') && $this->request->getFile('logo')->isValid(); if (! $hasLogo) { unset($rules['logo']); } if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $update = [ 'name' => strip_tags((string) $this->request->getPost('name')), 'slug' => (string) $this->request->getPost('slug'), 'description' => strip_tags((string) $this->request->getPost('description')), 'timezone' => (string) $this->request->getPost('timezone'), 'default_refresh' => (int) $this->request->getPost('default_refresh'), 'is_active' => (int) $this->request->getPost('is_active'), ]; if ($hasLogo) { $logo = $this->request->getFile('logo'); $logoName = $logo->getRandomName(); $logo->move(WRITEPATH . 'uploads/workspaces', $logoName); $update['logo'] = 'writable/uploads/workspaces/' . $logoName; } $workspaceModel->update($workspaceId, $update); (new SettingsModel())->setValue( $workspaceId, 'default_theme', (string) $this->request->getPost('default_theme'), 'string' ); AuditLogger::log( 'settings.workspace_saved', 'workspace', $workspaceId, null, ['name' => $update['name'], 'default_theme' => (string) $this->request->getPost('default_theme')], $workspaceId, (int) $this->session->get('user_id') ); return redirect()->to('/settings/workspace')->with('success', 'Workspace preferences saved.'); } public function notifications() { $settings = new SettingsModel(); $slackUrl = $settings->getValue(null, 'slack_webhook_url', '') ?? ''; $emailConfig = new EmailConfig(); return view('settings/notifications', [ 'title' => 'Notification settings | Chart-Board', 'smtpHost' => $emailConfig->SMTPHost, 'smtpPort' => $emailConfig->SMTPPort, 'smtpUser' => $emailConfig->SMTPUser, 'smtpCrypto' => $emailConfig->SMTPCrypto, 'fromEmail' => $emailConfig->fromEmail, 'slackWebhookUrl' => $slackUrl, ]); } public function saveSlackWebhook(): ResponseInterface { $rules = ['slack_webhook_url' => 'permit_empty|max_length[2000]']; if (! $this->validate($rules)) { return redirect()->back()->with('errors', $this->validator->getErrors()); } $url = trim((string) $this->request->getPost('slack_webhook_url')); (new SettingsModel())->setValue(null, 'slack_webhook_url', $url, 'string'); AuditLogger::log( 'settings.slack_webhook_saved', 'settings', null, null, ['slack_webhook_url' => $url !== '' ? '[redacted]' : 'cleared'], (int) $this->session->get('active_workspace_id') ?: null, (int) $this->session->get('user_id') ); return redirect()->to('/settings/notifications')->with('success', 'Slack webhook URL saved.'); } public function testSmtp(): ResponseInterface { $userId = (int) $this->session->get('user_id'); $user = (new \App\Models\UserModel())->find($userId); if (! $user) { return redirect()->back()->with('error', 'User not found.'); } $to = (string) ($user['email'] ?? ''); if (! filter_var($to, FILTER_VALIDATE_EMAIL)) { return redirect()->back()->with('error', 'Your profile email is invalid.'); } $config = new EmailConfig(); $email = Services::email(); $email->setFrom($config->fromEmail, $config->fromName); $email->setTo($to); $email->setSubject('Chart-Board SMTP test'); $email->setMessage('
This is a test message from Chart-Board. If you received it, SMTP is configured correctly.
'); if (! $email->send()) { return redirect()->back()->with('error', 'SMTP test failed. Check server mail configuration.'); } return redirect()->back()->with('success', 'Test email sent to your account email address.'); } public function testSlack(): ResponseInterface { $url = trim((string) (new SettingsModel())->getValue(null, 'slack_webhook_url', '') ?? ''); if ($url === '' || ! filter_var($url, FILTER_VALIDATE_URL)) { return redirect()->back()->with('error', 'Save a valid Slack webhook URL first.'); } $payload = json_encode(['text' => 'Chart-Board: Slack webhook test from ' . date('c')], JSON_UNESCAPED_UNICODE); $ch = curl_init($url); if ($ch === false) { return redirect()->back()->with('error', 'Could not start HTTP request.'); } curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => ['Content-Type: application/json'], CURLOPT_POSTFIELDS => $payload, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, ]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($code < 200 || $code >= 300) { return redirect()->back()->with('error', 'Slack returned HTTP ' . $code . '.'); } return redirect()->back()->with('success', 'Slack test message sent.'); } public function saveTheme() { $rules = ['theme' => 'required|in_list[light,dark,system]']; if (! $this->validate($rules)) { return redirect()->back()->with('error', 'Invalid theme.'); } $theme = (string) $this->request->getPost('theme'); $userId = (int) $this->session->get('user_id'); if ($userId <= 0) { return redirect()->to('/login'); } (new \App\Models\UserModel())->skipValidation(true)->update($userId, ['theme_preference' => $theme]); $this->session->set('theme_preference', $theme); return redirect()->back()->with('success', 'Theme preference saved.'); } }