Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore(subsite): list by origin suffix from backend (origin__endswith)",
"packageName": "@acedatacloud/nexior",
"email": "dev@acedata.cloud",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore(subsite): drop dead parent-id defensive filter",
"packageName": "@acedatacloud/nexior",
"email": "dev@acedata.cloud",
"dependentChangeType": "patch"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "chore(subsite): drop parentSite dependence and switch canCreate to isOfficial gate",
"packageName": "@acedatacloud/nexior",
"email": "dev@acedata.cloud",
"dependentChangeType": "patch"
}
80 changes: 56 additions & 24 deletions src/components/setting/Subsite.vue
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,16 @@ import { Plus } from '@element-plus/icons-vue';
import { siteOperator } from '@/operators';
import SectionNotice from '@/components/setting/SectionNotice.vue';
import type { ISite } from '@/models';
import { isOfficial, isSubOfficial } from '@/utils/is';
import { BASE_HOST_HUB } from '@/constants';

const SLUG_RE = /^(?!.*--)[a-z0-9][a-z0-9-]{1,30}[a-z0-9]$/;

/** Maximum number of subsites a single user can spin up. Mirrors the
* default the backend's subsite-create path enforces when the parent
* Site row hasn't been seeded with an explicit per-user quota. */
const MAX_SUBSITES_PER_USER = 5;

/**
* Settings tab — manage white-label subsites for the parent (official)
* Site row. Logic mirrors what used to live at the standalone
Expand Down Expand Up @@ -153,28 +160,41 @@ export default defineComponent({
};
},
computed: {
parentSite(): ISite | undefined {
return this.$store.state.site;
},
user() {
return this.$store.getters.user;
},
/**
* DNS suffix new subsites are spawned under — always the canonical
* Hub host (`BASE_HOST_HUB`, e.g. `studio.acedata.cloud`). The backend's
* subsite-create path pins ``origin = f"{slug}.{parent.origin}"`` against
* this same value, so we don't need to fetch the parent Site row just to
* read it back from ``parent.features.subsite.subdomain_zone``.
*/
subdomainZone(): string {
const zone = this.parentSite?.features?.subsite?.subdomain_zone;
if (zone) return zone;
// Fall back to the bare current host so the create dialog can render
// even if `state.site` hasn't been hydrated with the subsite block yet.
if (typeof window !== 'undefined' && window.location?.host) {
return window.location.host.split(':')[0];
}
return this.parentSite?.origin || '';
},
maxSubsitesPerUser(): number {
const max = this.parentSite?.features?.subsite?.max_subsites_per_user;
return typeof max === 'number' && max > 0 ? max : 5;
return BASE_HOST_HUB;
},
Comment on lines 173 to 175

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the studio host for subsite origins

On the only host where the Subsites tab is exposed (isMainOfficial() is studio.acedata.cloud), BASE_HOST_HUB resolves to hub.acedata.cloud (src/constants/endpoint.ts:4,9). Returning it here makes fetchSubsites() filter for origin__endswith=.hub.acedata.cloud and makes onSubmitCreate() post slug.hub.acedata.cloud, so existing *.studio.acedata.cloud subsites disappear and new creations target the wrong parent zone. Use the current/parent main host or a studio-specific constant instead of BASE_HOST_HUB.

Useful? React with 👍 / 👎.

/**
* Whether the current user can spin up another subsite right now.
*
* * Must be on the canonical Hub host (`isOfficial()`),
* * NOT on a subsite under it (`!isSubOfficial()`) — subsites can't
* themselves spawn further subsites,
* * Must be signed in (`user.id`),
* * Must be under the per-user quota.
*
* Mirrors PlatformFrontend's Sites console gate; replaces the previous
* `parent.features.subsite.{enabled,subdomain_zone}` lookup which required
* fetching the parent Site row just to gate one button + read back a
* value (`BASE_HOST_HUB`) that's already a build-time constant on this
* side of the wire.
*/
canCreate(): boolean {
return Boolean(this.subdomainZone) && Boolean(this.user?.id) && this.items.length < this.maxSubsitesPerUser;
return (
isOfficial() &&
!isSubOfficial() &&
Boolean(this.user?.id) &&
this.items.length < MAX_SUBSITES_PER_USER
);
Comment on lines 191 to +197

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid classifying the studio host as a subsite

When this tab is opened on studio.acedata.cloud, isOfficial() is true but isSubOfficial() also returns true because it only compares window.location.host with BASE_HOST_HUB (hub.acedata.cloud). Since Setting.vue exposes the Subsites tab specifically on studio.acedata.cloud, the new !isSubOfficial() check keeps canCreate false for the intended host and disables the normal Create button for all users there. Gate this with the strict main-host helper or fix isSubOfficial() before using it here.

Useful? React with 👍 / 👎.

}
},
watch: {
Expand All @@ -198,17 +218,29 @@ export default defineComponent({
this.items = [];
return;
}
const zone = this.subdomainZone;
if (!zone) {
// SSR / pre-mount path — BASE_HOST_HUB is a constant so this branch
// is effectively dead, but keep the guard cheap so we never POST
// a query with a blank suffix filter and end up listing every site
// the user owns under any host.
this.items = [];
return;
}
this.loading = true;
try {
const { data } = await siteOperator.getAll({ user_id: userId });
const all = (data?.items || []) as ISite[];
const parentId = this.parentSite?.id;
this.items = all.filter((s) => {
if (s.id === parentId) return false;
const meta = (s.metadata || {}) as Record<string, unknown>;
if (parentId && meta.parent_site_id) return meta.parent_site_id === parentId;
return Boolean(s.origin && this.subdomainZone && s.origin.endsWith(`.${this.subdomainZone}`));
// Listing is fully scoped by (user_id, origin__endswith=.{zone}).
// The leading dot excludes the parent (`studio.acedata.cloud`) by
// DNS-hierarchy semantics and matches every subsite
// (`<slug>.studio.acedata.cloud`). No `parent_site_id` needed —
// the superuser fast path doesn't stamp `metadata.parent_site_id`
// anyway, which is why the previous metadata filter hid rows.
const { data } = await siteOperator.getAll({
user_id: userId,
origin__endswith: `.${zone}`,
ordering: '-created_at'
});
this.items = (data?.items || []) as ISite[];
} catch (e) {
console.error('failed to load subsites', e);
ElMessage.error(this.$t('subsite.message.loadFailed'));
Expand Down
2 changes: 2 additions & 0 deletions src/operators/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { ISite, ISiteDetailResponse, ISiteListResponse } from '@/models';

export interface ISiteQuery {
origin?: string;
origin__endswith?: string;
user_id?: string;
ordering?: string;
offset?: number;
limit?: number;
}
Expand Down