mirror of
https://github.com/HeyPuter/puter.git
synced 2026-10-31 02:57:52 +00:00
fix(hosting): apps publish with write again (#4291)
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Backend Tests / changes (push) Canceled after 0s
Backend Tests / typecheck (push) Canceled after 0s
Backend Tests / test shard 1/4 (push) Canceled after 0s
Backend Tests / test shard 2/4 (push) Canceled after 0s
Backend Tests / test shard 3/4 (push) Canceled after 0s
Backend Tests / test shard 4/4 (push) Canceled after 0s
Backend Tests / test (main) (push) Canceled after 0s
Backend Tests / coverage (push) Canceled after 0s
Maintain Release Merge PR / update-release-pr (push) Canceled after 0s
Notify HeyPuter / notify (push) Canceled after 0s
release-please / release-please (push) Canceled after 0s
Reverts the hosting part of #4290. manage stays a user-to-user grant; an app with write on its user's directory can publish and keep serving it.
This commit is contained in:
1 parent
3bce3b056e
commit
5b0205f2bf
7 files changed
+38
-378
No files matched your search
@@ -26,8 +26,7 @@ import { PuterServer } from '../../../server';
|
||||
import { HOSTED_ASSET_COOKIES } from '../../../services/auth/AuthService';
|
||||
import { setupTestServer } from '../../../testUtil';
|
||||
import type { IConfig } from '../../../types';
|
||||
import { makeActor, type Actor } from '../../actor';
|
||||
import { runWithContext } from '../../context';
|
||||
import type { Actor } from '../../actor';
|
||||
import { generateDefaultFsentries } from '../../../util/userProvisioning';
|
||||
import { createPuterSiteMiddleware } from './puterSite';
|
||||
|
||||
@@ -1851,223 +1850,6 @@ describe('createPuterSiteMiddleware — delegated site roots', () => {
|
||||
expect((await serveSite(sub)).statusCode).toBe(404);
|
||||
});
|
||||
|
||||
it.each(['write', 'manage'])(
|
||||
'revalidates app-created sites with %s grants',
|
||||
async (mode) => {
|
||||
const owner = await makeUserWithHome();
|
||||
const app = await server.stores.app.create(
|
||||
{
|
||||
name: `publisher-${uuidv4()}`,
|
||||
title: 'Publisher',
|
||||
index_url: 'https://publisher.example.test/',
|
||||
},
|
||||
{ ownerUserId: owner.id },
|
||||
);
|
||||
const root = (await server.stores.fsEntry.getEntryByPath(
|
||||
`/${owner.username}/Documents`,
|
||||
))!;
|
||||
const permission =
|
||||
mode === 'manage'
|
||||
? `manage:fs:${root.uuid}`
|
||||
: `fs:${root.uuid}:write`;
|
||||
await server.services.permission.grantUserAppPermission(
|
||||
actorFor(owner),
|
||||
app.uid,
|
||||
permission,
|
||||
);
|
||||
const sub = `app-${Math.random().toString(36).slice(2, 10)}`;
|
||||
// Stored rows include publications made before the current authority check.
|
||||
await server.stores.subdomain.create({
|
||||
userId: owner.id,
|
||||
subdomain: sub,
|
||||
rootDirId: root.id,
|
||||
appOwner: app.id,
|
||||
});
|
||||
await writeFile(
|
||||
owner.id,
|
||||
`${root.path}/index.html`,
|
||||
Buffer.from('private initial'),
|
||||
'text/html',
|
||||
);
|
||||
expect((await serveSite(sub)).statusCode).toBe(
|
||||
mode === 'manage' ? 200 : 404,
|
||||
);
|
||||
await server.services.permission.revokeUserAppPermission(
|
||||
actorFor(owner),
|
||||
app.uid,
|
||||
permission,
|
||||
);
|
||||
await server.services.fs.write(owner.id, {
|
||||
fileMetadata: {
|
||||
path: `${root.path}/index.html`,
|
||||
overwrite: true,
|
||||
size: 14,
|
||||
contentType: 'text/html',
|
||||
},
|
||||
fileContent: Buffer.from('private future'),
|
||||
});
|
||||
const out = await serveSite(sub);
|
||||
expect(out.statusCode).toBe(404);
|
||||
expect(String(out.body)).not.toContain('private future');
|
||||
},
|
||||
);
|
||||
|
||||
it.each(['session', 'fullAccess', 'scoped'])(
|
||||
'root changes preserve publication authority for %s',
|
||||
async (mode) => {
|
||||
const owner = await makeUserWithHome();
|
||||
const userActor = makeActor({ user: owner });
|
||||
const actor =
|
||||
mode === 'session'
|
||||
? userActor
|
||||
: makeActor({
|
||||
user: owner,
|
||||
accessToken: {
|
||||
uid: uuidv4(),
|
||||
issuer: userActor,
|
||||
fullAccess: mode === 'fullAccess',
|
||||
},
|
||||
});
|
||||
const app = await server.stores.app.create(
|
||||
{
|
||||
name: `takeover-${uuidv4()}`,
|
||||
title: 'Publisher',
|
||||
index_url: 'https://publisher.example.test/',
|
||||
},
|
||||
{ ownerUserId: owner.id },
|
||||
);
|
||||
const appActor = makeActor({
|
||||
user: owner,
|
||||
app: { id: app.id, uid: app.uid },
|
||||
});
|
||||
const root = await server.services.fs.mkdir(owner.id, {
|
||||
path: `/${owner.username}/AppData/${app.uid}`,
|
||||
createMissingParents: true,
|
||||
});
|
||||
const sub = `takeover-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const site = await server.stores.subdomain.create({
|
||||
userId: owner.id,
|
||||
subdomain: sub,
|
||||
rootDirId: root.id,
|
||||
appOwner: app.id,
|
||||
});
|
||||
await runWithContext({ actor }, () =>
|
||||
server.drivers.subdomains.update({
|
||||
uid: site.uuid,
|
||||
object: { root_dir: root.path },
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(await server.stores.subdomain.getByUuid(site.uuid))!.app_owner,
|
||||
).toBe(app.id);
|
||||
await runWithContext({ actor }, () =>
|
||||
server.drivers.subdomains.update({
|
||||
uid: site.uuid,
|
||||
object: {},
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
(await server.stores.subdomain.getByUuid(site.uuid))!.app_owner,
|
||||
).toBe(app.id);
|
||||
const newRoot = `/${owner.username}/Documents`;
|
||||
if (mode === 'scoped') {
|
||||
const entry =
|
||||
(await server.stores.fsEntry.getEntryByPath(newRoot))!;
|
||||
await server.stores.permission.insertAccessTokenPerms(
|
||||
actor.accessToken!.uid,
|
||||
{ userId: owner.id, appId: null },
|
||||
[{ permission: `manage:fs:${entry.uuid}` }],
|
||||
);
|
||||
}
|
||||
await writeFile(
|
||||
owner.id,
|
||||
`${newRoot}/index.html`,
|
||||
Buffer.from('account publication'),
|
||||
'text/html',
|
||||
);
|
||||
await runWithContext({ actor }, () =>
|
||||
server.drivers.subdomains.update({
|
||||
uid: site.uuid,
|
||||
object: { root_dir: newRoot },
|
||||
}),
|
||||
);
|
||||
expect((await serveSite(sub)).statusCode).toBe(200);
|
||||
expect(
|
||||
(await server.stores.subdomain.getByUuid(site.uuid))!.app_owner,
|
||||
).toBeNull();
|
||||
await expect(
|
||||
runWithContext({ actor: appActor }, () =>
|
||||
server.drivers.subdomains.update({
|
||||
uid: site.uuid,
|
||||
object: { root_dir: root.path },
|
||||
}),
|
||||
),
|
||||
).rejects.toMatchObject({ statusCode: 403 });
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
'serves own AppData without grant queries (hosted app matches creator: %s)',
|
||||
async (hostedApp) => {
|
||||
const owner = await makeUserWithHome();
|
||||
const sub = `appdata-${Math.random().toString(36).slice(2, 10)}`;
|
||||
const app = await server.stores.app.create(
|
||||
{
|
||||
name: `data-${uuidv4()}`,
|
||||
title: 'Data',
|
||||
index_url: hostedApp
|
||||
? `http://${sub}.site.puter.localhost/`
|
||||
: 'https://data.example.test/',
|
||||
},
|
||||
{ ownerUserId: owner.id },
|
||||
);
|
||||
const root = await server.services.fs.mkdir(owner.id, {
|
||||
path: `/${owner.username}/AppData/${app.uid}`,
|
||||
createMissingParents: true,
|
||||
});
|
||||
await writeFile(
|
||||
owner.id,
|
||||
`${root.path}/index.html`,
|
||||
Buffer.from('app data'),
|
||||
'text/html',
|
||||
);
|
||||
await server.stores.subdomain.create({
|
||||
userId: owner.id,
|
||||
subdomain: sub,
|
||||
rootDirId: root.id,
|
||||
appOwner: app.id,
|
||||
});
|
||||
expect((await serveSite(sub)).statusCode).toBe(200);
|
||||
const read = vi.spyOn(server.clients.db, 'read');
|
||||
const get = vi.spyOn(server.clients.redis, 'get');
|
||||
const acl = vi.spyOn(server.services.acl, 'check');
|
||||
try {
|
||||
const out = await serveSite(sub);
|
||||
expect(out.statusCode).toBe(200);
|
||||
expect(acl).not.toHaveBeenCalled();
|
||||
expect(
|
||||
get.mock.calls.filter(
|
||||
([key]) => key === `apps:id:${app.id}`,
|
||||
),
|
||||
).toHaveLength(hostedApp ? 0 : 1);
|
||||
expect(
|
||||
read.mock.calls.filter(([query]) => {
|
||||
const sql = String(query);
|
||||
return (
|
||||
/(?:FROM|JOIN)\s+`?permissions`?\b/i.test(sql) ||
|
||||
(/FROM\s+`apps`/i.test(sql) &&
|
||||
!sql.includes('index_url'))
|
||||
);
|
||||
}),
|
||||
).toHaveLength(0);
|
||||
} finally {
|
||||
read.mockRestore();
|
||||
get.mockRestore();
|
||||
acl.mockRestore();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("never consults the ACL for a site rooted in the publisher's own tree", async () => {
|
||||
const owner = await makeUserWithHome();
|
||||
const homePath = `/${owner.username}`;
|
||||
@@ -2087,19 +1869,12 @@ describe('createPuterSiteMiddleware — delegated site roots', () => {
|
||||
});
|
||||
|
||||
const aclCheck = vi.spyOn(server.services.acl, 'check');
|
||||
const redisGet = vi.spyOn(server.clients.redis, 'get');
|
||||
try {
|
||||
const out = await serveSite(sub);
|
||||
expect(out.statusCode).toBe(200);
|
||||
expect(aclCheck).not.toHaveBeenCalled();
|
||||
expect(
|
||||
redisGet.mock.calls.filter(([key]) =>
|
||||
String(key).startsWith('apps:id:'),
|
||||
),
|
||||
).toHaveLength(0);
|
||||
} finally {
|
||||
aclCheck.mockRestore();
|
||||
redisGet.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,6 @@ import type { puterServices } from '../../../services';
|
||||
import { HOSTED_ASSET_COOKIES } from '../../../services/auth/AuthService';
|
||||
import { MANAGE_PERM_PREFIX } from '../../../services/permission/consts';
|
||||
import type { puterStores } from '../../../stores';
|
||||
import type { SubdomainRow } from '../../../stores/subdomain/SubdomainStore';
|
||||
import type { IConfig, LayerInstances } from '../../../types';
|
||||
import { awaitLater } from '../../../util/concurrency';
|
||||
import { makeActor } from '../../actor';
|
||||
@@ -142,6 +141,16 @@ const SUBDOMAIN_404 = `<div style="font-size: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;"><h1 style="margin:0; color:#727272;">404</h1><p style="margin-top:10px;">Subdomain or site is not pointing to a directory.</p></div>`;
|
||||
interface SubdomainRow {
|
||||
id: number;
|
||||
uuid: string;
|
||||
subdomain: string;
|
||||
user_id: number | null;
|
||||
root_dir_id: number | null;
|
||||
associated_app_id: number | null;
|
||||
domain?: string | null;
|
||||
protected?: number | null;
|
||||
}
|
||||
|
||||
interface AppRow {
|
||||
id: number;
|
||||
@@ -246,7 +255,7 @@ export const createPuterSiteMiddleware = (
|
||||
|
||||
const site = (await layers.stores.subdomain.getBySubdomain(
|
||||
subdomain,
|
||||
)) as SubdomainRow | null;
|
||||
)) as unknown as SubdomainRow | null;
|
||||
if (!site || site.user_id === null || site.user_id === undefined) {
|
||||
res.status(404).type('text/plain').send('Subdomain not found');
|
||||
return;
|
||||
@@ -529,32 +538,13 @@ export const createPuterSiteMiddleware = (
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegated publications depend on their current share authority.
|
||||
// Account-owned roots keep their direct path without extra lookups.
|
||||
const publisherApp =
|
||||
site.app_owner == null
|
||||
? null
|
||||
: Number(associatedApp?.id) === Number(site.app_owner)
|
||||
? associatedApp
|
||||
: await layers.stores.app.getById(Number(site.app_owner));
|
||||
const ownAppDataPath = publisherApp
|
||||
? `/${owner.username}/AppData/${publisherApp.uid}`
|
||||
: null;
|
||||
const ownAppData =
|
||||
rootEntry.userId === site.user_id &&
|
||||
ownAppDataPath &&
|
||||
(rootEntry.path === ownAppDataPath ||
|
||||
rootEntry.path.startsWith(`${ownAppDataPath}/`));
|
||||
if (site.app_owner != null && !publisherApp) {
|
||||
res.status(404)
|
||||
.type('text/html; charset=UTF-8')
|
||||
.send(SUBDOMAIN_404);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
rootEntry.userId !== site.user_id ||
|
||||
(publisherApp && !ownAppData)
|
||||
) {
|
||||
// A site rooted in someone else's directory stands on the `manage`
|
||||
// grant that authorized publishing it, and serves that whole subtree
|
||||
// with the ACL bypassed — so the grant is re-read on every request.
|
||||
// Trash is its own check: a grant is keyed on the node, not its
|
||||
// location, so it survives the owner deleting the directory. Sites
|
||||
// rooted in their publisher's own tree skip all of this.
|
||||
if (rootEntry.userId !== site.user_id) {
|
||||
let stillPublishable = false;
|
||||
if (!isTrashedPath(rootEntry.path)) {
|
||||
try {
|
||||
@@ -565,9 +555,6 @@ export const createPuterSiteMiddleware = (
|
||||
uuid: owner.uuid,
|
||||
username: owner.username,
|
||||
},
|
||||
app: publisherApp
|
||||
? { id: publisherApp.id, uid: publisherApp.uid }
|
||||
: null,
|
||||
}),
|
||||
{
|
||||
path: rootEntry.path,
|
||||
|
||||
@@ -645,102 +645,6 @@ describe('SubdomainDriver root_dir publishing rights', () => {
|
||||
).rejects.toMatchObject({ statusCode: 403 });
|
||||
});
|
||||
|
||||
it.each(['create', 'update'])(
|
||||
'refuses a same-user write-only app on %s',
|
||||
async (operation) => {
|
||||
const owner = await makeUser();
|
||||
const app = await server.stores.app.create(
|
||||
{
|
||||
name: uniqueSubdomain('publisher'),
|
||||
title: 'Publisher',
|
||||
index_url: 'https://publisher.example.test/',
|
||||
},
|
||||
{ ownerUserId: owner.userId },
|
||||
);
|
||||
const actor = makeActor({
|
||||
user: owner.actor.user,
|
||||
app: { id: app.id, uid: app.uid },
|
||||
});
|
||||
const shared = `/${owner.actor.user!.username}/Documents`;
|
||||
const entry = (await server.stores.fsEntry.getEntryByPath(shared))!;
|
||||
await server.services.permission.grantUserAppPermission(
|
||||
owner.actor,
|
||||
app.uid,
|
||||
`fs:${entry.uuid}:write`,
|
||||
);
|
||||
let uid: unknown;
|
||||
if (operation === 'update') {
|
||||
const root = `/${owner.actor.user!.username}/AppData/${app.uid}`;
|
||||
await server.services.fs.mkdir(owner.userId, {
|
||||
path: root,
|
||||
createMissingParents: true,
|
||||
});
|
||||
const created = (await withActor(actor, () =>
|
||||
driver.create({
|
||||
object: {
|
||||
subdomain: uniqueSubdomain('app-root'),
|
||||
root_dir: root,
|
||||
},
|
||||
}),
|
||||
)) as Record<string, unknown>;
|
||||
uid = created.uid;
|
||||
}
|
||||
await expect(
|
||||
withActor(actor, () =>
|
||||
operation === 'create'
|
||||
? driver.create({
|
||||
object: {
|
||||
subdomain: uniqueSubdomain('denied-app'),
|
||||
root_dir: shared,
|
||||
},
|
||||
})
|
||||
: driver.update({ uid, object: { root_dir: shared } }),
|
||||
),
|
||||
).rejects.toMatchObject({ statusCode: 403 });
|
||||
},
|
||||
);
|
||||
|
||||
it('accepts a same-user app with share authority', async () => {
|
||||
const owner = await makeUser();
|
||||
const app = await server.stores.app.create(
|
||||
{
|
||||
name: uniqueSubdomain('manager'),
|
||||
title: 'Manager',
|
||||
index_url: 'https://manager.example.test/',
|
||||
},
|
||||
{ ownerUserId: owner.userId },
|
||||
);
|
||||
const root = (await server.stores.fsEntry.getEntryByPath(
|
||||
`/${owner.actor.user!.username}/Documents`,
|
||||
))!;
|
||||
await server.services.permission.grantUserAppPermission(
|
||||
owner.actor,
|
||||
app.uid,
|
||||
`manage:fs:${root.uuid}`,
|
||||
);
|
||||
const actor = makeActor({
|
||||
user: owner.actor.user,
|
||||
app: { id: app.id, uid: app.uid },
|
||||
});
|
||||
const sub = uniqueSubdomain('managed-app');
|
||||
const result = (await withActor(actor, () =>
|
||||
driver.create({ object: { subdomain: sub, root_dir: root.path } }),
|
||||
)) as Record<string, unknown>;
|
||||
expect(result.subdomain).toBe(sub);
|
||||
const nextRoot = (await server.stores.fsEntry.getEntryByPath(
|
||||
`/${owner.actor.user!.username}/Public`,
|
||||
))!;
|
||||
await server.services.permission.grantUserAppPermission(
|
||||
owner.actor, app.uid, `manage:fs:${nextRoot.uuid}`,
|
||||
);
|
||||
await withActor(actor, () => driver.update({
|
||||
uid: result.uid, object: { root_dir: nextRoot.path },
|
||||
}));
|
||||
expect((await server.stores.subdomain.getBySubdomain(sub))!.app_owner)
|
||||
.toBe(app.id);
|
||||
|
||||
});
|
||||
|
||||
// "Can edit & share" is the owner delegating the decision.
|
||||
it('accepts a directory shared with the actor at manage', async () => {
|
||||
const owner = await makeUser();
|
||||
@@ -783,6 +687,8 @@ describe('SubdomainDriver root_dir publishing rights', () => {
|
||||
).rejects.toMatchObject({ statusCode: 403 });
|
||||
});
|
||||
|
||||
// The owner's own path still only needs write, so an app hosting a
|
||||
// directory its user gave it keeps working.
|
||||
it("still accepts the actor's own directory", async () => {
|
||||
const { actor } = await makeUser();
|
||||
const sub = uniqueSubdomain('own');
|
||||
|
||||
@@ -357,7 +357,6 @@ export class SubdomainDriver extends PuterDriver {
|
||||
|
||||
// Subdomain name is immutable — strip if provided
|
||||
const patch: Record<string, unknown> = {};
|
||||
let claimForAccount = false;
|
||||
if (object.root_dir !== undefined) {
|
||||
const rootDirPath = expandTildePath(
|
||||
String(object.root_dir),
|
||||
@@ -372,8 +371,6 @@ export class SubdomainDriver extends PuterDriver {
|
||||
}
|
||||
if (rootDirId !== (row.root_dir_id ?? null)) {
|
||||
await this.#checkPublishAccess(entry, actor);
|
||||
// Scoped account credentials have proved manage on the new root.
|
||||
claimForAccount = actor.effectiveApp === null;
|
||||
}
|
||||
patch.root_dir_id = rootDirId;
|
||||
}
|
||||
@@ -384,7 +381,7 @@ export class SubdomainDriver extends PuterDriver {
|
||||
const updated = await this.stores.subdomain.update(
|
||||
String(row.uuid),
|
||||
patch,
|
||||
{ userId: row.user_id as number, claimForAccount },
|
||||
{ userId: row.user_id as number },
|
||||
);
|
||||
const [shaped] = await this.#hydrateRows(
|
||||
updated ? [updated as Record<string, unknown>] : [],
|
||||
@@ -564,11 +561,22 @@ export class SubdomainDriver extends PuterDriver {
|
||||
throw new HttpError(403, 'Access denied', { legacyCode: 'forbidden' });
|
||||
}
|
||||
|
||||
// Publishing exposes future descendants too, so every actor needs share authority.
|
||||
/**
|
||||
* Gate on pointing a subdomain at a directory. Hosting serves everything
|
||||
* under the root dir with the ACL bypassed, so publishing makes that
|
||||
* subtree world-readable — for good, and including whatever is written
|
||||
* there later. That is the owner's call to make.
|
||||
*
|
||||
* `write` is not it. A shared folder's writer would be exposing the owner's
|
||||
* tree, the row belongs to the writer's account, and nothing the owner can
|
||||
* list would show it. `manage` — "Can edit & share" — is the grant that
|
||||
* delegates the decision.
|
||||
*/
|
||||
async #checkPublishAccess(entry: FSEntry, actor: Actor): Promise<void> {
|
||||
// First, because this is the check that masks a directory the caller
|
||||
// cannot see at all as a 404.
|
||||
await this.services.fs.checkFSAccess(entry, actor);
|
||||
if (entry.userId === actor.user?.id) return;
|
||||
try {
|
||||
await this.services.fs.checkFSAccess(
|
||||
entry,
|
||||
|
||||
@@ -63,8 +63,7 @@ export type CascadeRow = Pick<
|
||||
// - name / identity: `subdomain` — v1 marks this `immutable: true`; rename
|
||||
// would orphan DNS + ACL wiring tied to the old name.
|
||||
// - ownership: `user_id`, `app_owner` — set via `create` (or reclaimed in
|
||||
// place by `reclaimByUuid`), never via patch. Account root reassignment
|
||||
// clears app ownership through a trusted store option.
|
||||
// place by `reclaimByUuid`), never via patch.
|
||||
// Flipping either hands the site to another user / app.
|
||||
// - access gate: `protected` — clearing this lets a future caller delete
|
||||
// or rename a protected site (e.g. `puter-app-icons`). The driver itself
|
||||
@@ -611,11 +610,8 @@ export class SubdomainStore extends PuterStore {
|
||||
patch: Record<string, unknown>,
|
||||
{
|
||||
userId,
|
||||
claimForAccount = false,
|
||||
}: {
|
||||
userId?: number | undefined;
|
||||
/** The account explicitly selected a different publication root. */
|
||||
claimForAccount?: boolean;
|
||||
} = {},
|
||||
) {
|
||||
const allowed: Record<string, unknown> = {};
|
||||
@@ -623,19 +619,11 @@ export class SubdomainStore extends PuterStore {
|
||||
if (READ_ONLY_COLUMNS.has(k)) continue;
|
||||
allowed[k] = v;
|
||||
}
|
||||
if (Object.keys(allowed).length === 0)
|
||||
return this.getByUuid(uuid, { userId });
|
||||
const keys = Object.keys(allowed);
|
||||
if (keys.length === 0) return this.getByUuid(uuid, { userId });
|
||||
|
||||
// Rename drops the old cache key.
|
||||
const before = await this.getByUuid(uuid, { userId });
|
||||
if (
|
||||
claimForAccount &&
|
||||
allowed.root_dir_id !== undefined &&
|
||||
allowed.root_dir_id !== before?.root_dir_id
|
||||
) {
|
||||
allowed.app_owner = null;
|
||||
}
|
||||
const keys = Object.keys(allowed);
|
||||
|
||||
const setClause = keys.map((k) => `\`${k}\` = ?`).join(', ');
|
||||
const values = keys.map((k) => allowed[k]);
|
||||
|
||||
@@ -23,7 +23,7 @@ A string containing the name of the subdomain you want to create.
|
||||
|
||||
A string containing the path to the directory you want to serve.
|
||||
|
||||
Hosting serves the directory publicly, including files added later. An app can publish its own app data directory. Publishing another directory requires `manage` access, which allows editing and sharing. A shared directory stops being served if that access is withdrawn or its owner moves it to Trash.
|
||||
The directory must be one you own. Hosting serves everything under it publicly, including files added later, so a directory someone shared with you can only be published if they gave you `manage` access — [`share()`](/FS/share/) calls that level "Can edit & share". That access is also what keeps the site up: if the owner withdraws it, or moves the directory to their trash, the site stops being served.
|
||||
|
||||
#### `options` (Object) (optional)
|
||||
|
||||
|
||||
@@ -22,10 +22,6 @@ A string containing the name of the subdomain you want to update.
|
||||
|
||||
A string containing the path to the directory you want to serve.
|
||||
|
||||
An app can publish its own app data directory. Publishing another directory requires `manage` access, which allows editing and sharing. Hosting serves all files under the directory publicly, including files added later. A shared directory stops being served if that access is withdrawn or its owner moves it to Trash.
|
||||
|
||||
When an account credential selects a different directory for an app-created site, the account takes ownership of the publication and the creating app loses its management access. This includes an account-issued access token with management authority over the new directory. Selecting the current directory preserves the creating app's access. Credentials acting through an app preserve that app's ownership.
|
||||
|
||||
## Return value
|
||||
|
||||
A `Promise` that will resolve to a [`Subdomain`](/Objects/subdomain/) object when the subdomain has been updated. If a subdomain with the given name does not exist, the promise will be rejected with an error. If the path does not exist, the promise will be rejected with an error.
|
||||
|
||||
Reference in new issue
Block a user