shared group provisioning, upload context menu, go path fix
- provision linux users with pastilhas:officerdev ownership so server can always read/write, terminal users get group access - add officerdev shared group setup to setup.sh - move go install to ~/.local/go with GOPATH at ~/.local/go-path - add upload file/folder items to file browser context menu Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,24 @@ echo "════════════════════════
|
|||||||
echo " Officer — dependency setup ($PM)"
|
echo " Officer — dependency setup ($PM)"
|
||||||
echo "═══════════════════════════════════════════"
|
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 ───────────────────────────────────────────────────
|
# ─── 1. core system packages ───────────────────────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Core system packages ──"
|
echo "── Core system packages ──"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DATA_PATH, getHomeDir, toShellUsername } from '@@/data-path';
|
|||||||
import { generateContainerContext, generateClaudeSettings } from '@@/generate-container-context';
|
import { generateContainerContext, generateClaudeSettings } from '@@/generate-container-context';
|
||||||
|
|
||||||
const TEMPLATE_DIR = join(import.meta.dir, '../terminal/templates');
|
const TEMPLATE_DIR = join(import.meta.dir, '../terminal/templates');
|
||||||
|
const SHARED_GROUP = 'officerdev';
|
||||||
|
|
||||||
const run = (cmd: string[], opts?: { cwd?: string }): boolean => {
|
const run = (cmd: string[], opts?: { cwd?: string }): boolean => {
|
||||||
const result = Bun.spawnSync({ cmd, stdout: 'ignore', stderr: 'pipe', ...opts });
|
const result = Bun.spawnSync({ cmd, stdout: 'ignore', stderr: 'pipe', ...opts });
|
||||||
@@ -18,12 +19,27 @@ const linuxUserExists = (username: string): boolean => {
|
|||||||
return result.exitCode === 0;
|
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) => {
|
const copyTemplate = async (src: string, dest: string) => {
|
||||||
if (existsSync(dest)) return;
|
if (existsSync(dest)) return;
|
||||||
const content = await Bun.file(src).text();
|
const content = await Bun.file(src).text();
|
||||||
await Bun.write(dest, content);
|
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<boolean> {
|
export async function provisionLinuxUser(email: string, username: string): Promise<boolean> {
|
||||||
const shellUsername = toShellUsername(username, email);
|
const shellUsername = toShellUsername(username, email);
|
||||||
const homeDir = getHomeDir(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}`);
|
console.log(`[provision] provisioning Linux user ${shellUsername} for ${email}`);
|
||||||
|
|
||||||
|
// Ensure shared group exists
|
||||||
|
ensureSharedGroup();
|
||||||
|
|
||||||
// Ensure data directories exist
|
// Ensure data directories exist
|
||||||
mkdirSync(userRoot, { recursive: true });
|
mkdirSync(userRoot, { recursive: true });
|
||||||
mkdirSync(homeDir, { recursive: true });
|
mkdirSync(homeDir, { recursive: true });
|
||||||
|
|
||||||
// Create Linux user if not exists
|
// Create Linux user if not exists
|
||||||
if (!linuxUserExists(shellUsername)) {
|
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) {
|
if (!ok) {
|
||||||
console.error(`[provision] failed to create Linux user ${shellUsername}`);
|
console.error(`[provision] failed to create Linux user ${shellUsername}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
console.log(`[provision] created Linux user ${shellUsername}`);
|
console.log(`[provision] created Linux user ${shellUsername}`);
|
||||||
} else {
|
} else {
|
||||||
|
// Ensure existing user is in the shared group
|
||||||
|
run(['sudo', 'usermod', '-aG', SHARED_GROUP, shellUsername]);
|
||||||
console.log(`[provision] Linux user ${shellUsername} already exists`);
|
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
|
// Seed shell config files
|
||||||
await seedShellConfigs(homeDir);
|
await seedShellConfigs(homeDir);
|
||||||
|
|
||||||
@@ -66,7 +76,6 @@ export async function provisionLinuxUser(email: string, username: string): Promi
|
|||||||
const claudeDir = join(homeDir, '.claude');
|
const claudeDir = join(homeDir, '.claude');
|
||||||
mkdirSync(claudeDir, { recursive: true });
|
mkdirSync(claudeDir, { recursive: true });
|
||||||
|
|
||||||
// Copy context to user's .claude dir
|
|
||||||
const contextContent = await Bun.file(contextFile).text();
|
const contextContent = await Bun.file(contextFile).text();
|
||||||
await Bun.write(join(claudeDir, 'CLAUDE.md'), contextContent);
|
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();
|
const settingsContent = await Bun.file(settingsFile).text();
|
||||||
await Bun.write(join(claudeDir, 'settings.json'), settingsContent);
|
await Bun.write(join(claudeDir, 'settings.json'), settingsContent);
|
||||||
|
|
||||||
// Fix ownership and permissions after seeding
|
// Service user (pastilhas) owns everything — server can always read/write.
|
||||||
run(['sudo', 'chown', '-R', `${shellUsername}:${shellUsername}`, userRoot]);
|
// Linux user gets group access via officerdev for terminal sessions only.
|
||||||
run(['sudo', 'chmod', '-R', '770', userRoot]);
|
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}`);
|
console.log(`[provision] provisioning complete for ${shellUsername}`);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+31
-1
@@ -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 { getIcon } from 'material-file-icons';
|
||||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||||
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
|
import type { UseFileBrowserAppType } from '../useFileBrowserApp';
|
||||||
@@ -29,8 +30,19 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
|
|||||||
handleCreateDashboardHere,
|
handleCreateDashboardHere,
|
||||||
setShowVideoDownload,
|
setShowVideoDownload,
|
||||||
setShowDictate,
|
setShowDictate,
|
||||||
|
handleUpload,
|
||||||
} = fileBrowserManager;
|
} = fileBrowserManager;
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
const folderInputRef = useRef<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const handleFileChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (ev.target.files?.length) {
|
||||||
|
handleUpload(ev.target.files);
|
||||||
|
ev.target.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex-1 min-h-0 relative"
|
className="flex-1 min-h-0 relative"
|
||||||
@@ -121,6 +133,14 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
|
|||||||
<FolderPlus className="mr-2 h-4 w-4" />
|
<FolderPlus className="mr-2 h-4 w-4" />
|
||||||
New folder
|
New folder
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={() => fileInputRef.current?.click()} className="cursor-pointer">
|
||||||
|
<Upload className="mr-2 h-4 w-4" />
|
||||||
|
Upload files
|
||||||
|
</ContextMenuItem>
|
||||||
|
<ContextMenuItem onClick={() => folderInputRef.current?.click()} className="cursor-pointer">
|
||||||
|
<FolderUp className="mr-2 h-4 w-4" />
|
||||||
|
Upload folder
|
||||||
|
</ContextMenuItem>
|
||||||
<ContextMenuItem onClick={handleCreateDashboardHere} className="cursor-pointer">
|
<ContextMenuItem onClick={handleCreateDashboardHere} className="cursor-pointer">
|
||||||
<LayoutGrid className="mr-2 h-4 w-4" />
|
<LayoutGrid className="mr-2 h-4 w-4" />
|
||||||
Create Dashboard here
|
Create Dashboard here
|
||||||
@@ -136,6 +156,16 @@ export const FileViewContainer = ({ fileBrowserManager }: FileViewContainerProps
|
|||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
)}
|
)}
|
||||||
|
<input ref={fileInputRef} type="file" multiple className="hidden" onChange={handleFileChange} />
|
||||||
|
<input
|
||||||
|
ref={(input) => {
|
||||||
|
folderInputRef.current = input;
|
||||||
|
if (input) input.setAttribute('webkitdirectory', '');
|
||||||
|
}}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user