diff --git a/scripts/setup.sh b/scripts/setup.sh index f32f39ef..8775ef1b 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -45,6 +45,24 @@ echo "════════════════════════ echo " Officer — dependency setup ($PM)" echo "═══════════════════════════════════════════" +# ─── 0. shared group ────────────────────────────────────────────────────────── +echo "" +echo "── Shared group (officerdev) ──" + +if getent group officerdev &>/dev/null; then + skip "officerdev group" +else + sudo groupadd officerdev + ok "created officerdev group" +fi + +if id -nG "$USER" | grep -qw officerdev; then + skip "$USER in officerdev group" +else + sudo usermod -aG officerdev "$USER" + ok "added $USER to officerdev group" +fi + # ─── 1. core system packages ─────────────────────────────────────────────────── echo "" echo "── Core system packages ──" diff --git a/src/servers/api/users/provision.ts b/src/servers/api/users/provision.ts index b28b86b0..135351cc 100644 --- a/src/servers/api/users/provision.ts +++ b/src/servers/api/users/provision.ts @@ -4,6 +4,7 @@ import { DATA_PATH, getHomeDir, toShellUsername } from '@@/data-path'; import { generateContainerContext, generateClaudeSettings } from '@@/generate-container-context'; const TEMPLATE_DIR = join(import.meta.dir, '../terminal/templates'); +const SHARED_GROUP = 'officerdev'; const run = (cmd: string[], opts?: { cwd?: string }): boolean => { const result = Bun.spawnSync({ cmd, stdout: 'ignore', stderr: 'pipe', ...opts }); @@ -18,12 +19,27 @@ const linuxUserExists = (username: string): boolean => { return result.exitCode === 0; }; +const groupExists = (group: string): boolean => { + const result = Bun.spawnSync({ cmd: ['getent', 'group', group], stdout: 'ignore', stderr: 'ignore' }); + return result.exitCode === 0; +}; + const copyTemplate = async (src: string, dest: string) => { if (existsSync(dest)) return; const content = await Bun.file(src).text(); await Bun.write(dest, content); }; +export function ensureSharedGroup(): void { + if (groupExists(SHARED_GROUP)) return; + run(['sudo', 'groupadd', SHARED_GROUP]); + const serviceUser = process.env.USER ?? ''; + if (serviceUser) { + run(['sudo', 'usermod', '-aG', SHARED_GROUP, serviceUser]); + } + console.log(`[provision] created shared group ${SHARED_GROUP} and added ${serviceUser}`); +} + export async function provisionLinuxUser(email: string, username: string): Promise { const shellUsername = toShellUsername(username, email); const homeDir = getHomeDir(email); @@ -31,33 +47,27 @@ export async function provisionLinuxUser(email: string, username: string): Promi console.log(`[provision] provisioning Linux user ${shellUsername} for ${email}`); + // Ensure shared group exists + ensureSharedGroup(); + // Ensure data directories exist mkdirSync(userRoot, { recursive: true }); mkdirSync(homeDir, { recursive: true }); // Create Linux user if not exists if (!linuxUserExists(shellUsername)) { - const ok = run(['sudo', 'useradd', '-d', homeDir, '-s', '/bin/zsh', '-M', shellUsername]); + const ok = run(['sudo', 'useradd', '-d', homeDir, '-s', '/bin/zsh', '-G', SHARED_GROUP, '-M', shellUsername]); if (!ok) { console.error(`[provision] failed to create Linux user ${shellUsername}`); return false; } console.log(`[provision] created Linux user ${shellUsername}`); } else { + // Ensure existing user is in the shared group + run(['sudo', 'usermod', '-aG', SHARED_GROUP, shellUsername]); console.log(`[provision] Linux user ${shellUsername} already exists`); } - // Set ownership and permissions on user data directory - // chmod 770 so only owner and group can access (service user is added to group below) - run(['sudo', 'chown', '-R', `${shellUsername}:${shellUsername}`, userRoot]); - run(['sudo', 'chmod', '-R', '770', userRoot]); // Recursive chmod to fix all subdirectories - - // Add the service user to the new user's group so server jobs can access user data - const serviceUser = process.env.USER ?? ''; - if (serviceUser && serviceUser !== shellUsername) { - run(['sudo', 'usermod', '-aG', shellUsername, serviceUser]); - } - // Seed shell config files await seedShellConfigs(homeDir); @@ -66,7 +76,6 @@ export async function provisionLinuxUser(email: string, username: string): Promi const claudeDir = join(homeDir, '.claude'); mkdirSync(claudeDir, { recursive: true }); - // Copy context to user's .claude dir const contextContent = await Bun.file(contextFile).text(); await Bun.write(join(claudeDir, 'CLAUDE.md'), contextContent); @@ -74,9 +83,11 @@ export async function provisionLinuxUser(email: string, username: string): Promi const settingsContent = await Bun.file(settingsFile).text(); await Bun.write(join(claudeDir, 'settings.json'), settingsContent); - // Fix ownership and permissions after seeding - run(['sudo', 'chown', '-R', `${shellUsername}:${shellUsername}`, userRoot]); - run(['sudo', 'chmod', '-R', '770', userRoot]); + // Service user (pastilhas) owns everything — server can always read/write. + // Linux user gets group access via officerdev for terminal sessions only. + const serviceUser = process.env.USER ?? 'pastilhas'; + run(['sudo', 'chown', '-R', `${serviceUser}:${SHARED_GROUP}`, userRoot]); + run(['sudo', 'chmod', '-R', '2775', userRoot]); console.log(`[provision] provisioning complete for ${shellUsername}`); return true; diff --git a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx index 80b36dce..ec965e77 100644 --- a/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx +++ b/src/workspaces/officerdev/src/apps/FileBrowser/FileBrowserApp/components/FileViewContainer.tsx @@ -1,4 +1,5 @@ -import { Loader2, Folder, ClipboardPaste, FolderPlus, LayoutGrid, Upload, ClipboardCopy, MessageSquare, Download, Mic } from 'lucide-react'; +import { useRef } from 'react'; +import { Loader2, Folder, ClipboardPaste, FolderPlus, FolderUp, LayoutGrid, Upload, ClipboardCopy, MessageSquare, Download, Mic } from 'lucide-react'; import { getIcon } from 'material-file-icons'; import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu'; import type { UseFileBrowserAppType } from '../useFileBrowserApp'; @@ -29,8 +30,19 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps handleCreateDashboardHere, setShowVideoDownload, setShowDictate, + handleUpload, } = fileBrowserManager; + const fileInputRef = useRef(null); + const folderInputRef = useRef(null); + + const handleFileChange = (ev: React.ChangeEvent) => { + if (ev.target.files?.length) { + handleUpload(ev.target.files); + ev.target.value = ''; + } + }; + return (
New folder + fileInputRef.current?.click()} className="cursor-pointer"> + + Upload files + + folderInputRef.current?.click()} className="cursor-pointer"> + + Upload folder + Create Dashboard here @@ -136,6 +156,16 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps )} + + { + folderInputRef.current = input; + if (input) input.setAttribute('webkitdirectory', ''); + }} + type="file" + className="hidden" + onChange={handleFileChange} + />
); };