This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* deleteUserFolder — removes a user's filesystem folder on account deletion.
|
||||
*
|
||||
* The DB cascade-deletes owned rows, but the user folder
|
||||
* ({root}/{ownerId}/...) is on disk and was previously orphaned on delete.
|
||||
* See docs/superpowers/plans/2026-06-09-local-auth.md.
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { deleteUserFolder, ensureUserFolder } from './paths.js';
|
||||
|
||||
describe('deleteUserFolder', () => {
|
||||
let root = '';
|
||||
beforeEach(() => { root = mkdtempSync(join(tmpdir(), 'maestro-uf-')); });
|
||||
afterEach(() => { if (root) { rmSync(root, { recursive: true, force: true }); root = ''; } });
|
||||
|
||||
it('removes the entire user folder', () => {
|
||||
const dir = ensureUserFolder(root, 'alice');
|
||||
writeFileSync(join(dir, 'notes', 'x.md'), 'hi');
|
||||
expect(existsSync(dir)).toBe(true);
|
||||
deleteUserFolder(root, 'alice');
|
||||
expect(existsSync(dir)).toBe(false);
|
||||
});
|
||||
|
||||
it('NEVER removes the local/system folder (shared no-auth owner)', () => {
|
||||
const dir = ensureUserFolder(root, 'local');
|
||||
deleteUserFolder(root, 'local');
|
||||
expect(existsSync(dir)).toBe(true); // preserved
|
||||
});
|
||||
|
||||
it('is a no-op when the folder does not exist', () => {
|
||||
expect(() => deleteUserFolder(root, 'ghost')).not.toThrow();
|
||||
});
|
||||
|
||||
it('refuses path-traversal ids (no escape outside root)', () => {
|
||||
// userRoot rejects these; deleteUserFolder must not delete anything outside.
|
||||
const sibling = mkdtempSync(join(tmpdir(), 'maestro-sibling-'));
|
||||
writeFileSync(join(sibling, 'keep.txt'), 'keep');
|
||||
expect(() => deleteUserFolder(root, '../../etc')).not.toThrow();
|
||||
expect(existsSync(join(sibling, 'keep.txt'))).toBe(true);
|
||||
rmSync(sibling, { recursive: true, force: true });
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
import { mkdirSync, chmodSync, existsSync, readFileSync, writeFileSync, unlinkSync, statSync, openSync, readSync, closeSync } from 'fs';
|
||||
import { mkdirSync, chmodSync, existsSync, readFileSync, writeFileSync, unlinkSync, statSync, openSync, readSync, closeSync, rmSync } from 'fs';
|
||||
import { resolve, join, relative, isAbsolute } from 'path';
|
||||
|
||||
/** The shared no-auth / local-auth system owner. Its folder is never deleted. */
|
||||
export const LOCAL_SYSTEM_OWNER_ID = 'local';
|
||||
|
||||
const USER_AGENTS_MAX_BYTES = 64 * 1024;
|
||||
|
||||
export const USER_SUBDIRS = ['scripts', 'browser-macros', 'templates', 'recordings', 'trash', 'memory', 'pets', 'notes'] as const;
|
||||
@@ -29,6 +32,29 @@ export function ensureUserFolder(rootDir: string, ownerId: string): string {
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively remove a user's entire folder ({root}/{ownerId}). Called when an
|
||||
* account is deleted so its scripts/macros/recordings/notes/memory/pets don't
|
||||
* orphan on disk (the DB cascade-deletes rows but not files).
|
||||
*
|
||||
* Guards:
|
||||
* - the `local` system owner is NEVER removed (shared no-auth / local-auth
|
||||
* admin; deleting it would wipe the single-user deployment's data)
|
||||
* - an invalid / traversal ownerId resolves to nothing and is a silent no-op
|
||||
* (userRoot throws on '/', '\\', '\0', absolute paths)
|
||||
* - missing folder is a no-op (rmSync force)
|
||||
*/
|
||||
export function deleteUserFolder(rootDir: string, ownerId: string): void {
|
||||
if (ownerId === LOCAL_SYSTEM_OWNER_ID) return;
|
||||
let root: string;
|
||||
try {
|
||||
root = userRoot(rootDir, ownerId);
|
||||
} catch {
|
||||
return; // invalid ownerId — never delete anything outside a valid user root
|
||||
}
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
export function resolveUserSubdir(
|
||||
rootDir: string,
|
||||
ownerId: string,
|
||||
|
||||
Reference in New Issue
Block a user