mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-20 04:05:58 +00:00
feat: render the teams UI for allowlisted users without the global switch
`gui_params.teams_ui` stays the deployment-wide switch; with it off, the tab now also renders for a signed-in user who passes the email-domain allowlist (membership included, so seats see their roster). Anonymous renders hide it, and the API keeps deciding real access either way.
This commit is contained in:
@@ -301,8 +301,9 @@
|
||||
// free cap meaningful.
|
||||
// "max_seats_per_team": 50,
|
||||
//
|
||||
// Staged rollout: only these email domains may create a team or see the
|
||||
// tab. Members of an existing team always pass. Unset means everyone.
|
||||
// Staged rollout: only these email domains may create a team, and the tab
|
||||
// renders for them even with `gui_params.teams_ui` off. Members of an
|
||||
// existing team always pass. Unset means everyone, gated by `teams_ui`.
|
||||
// "teams_allowed_email_domains": ["puter.com"],
|
||||
|
||||
// ── Notifications ───────────────────────────────────────────────────
|
||||
|
||||
@@ -29,12 +29,13 @@ type EmitAndWait = (key: string, event: unknown, meta: unknown) => unknown;
|
||||
const makeService = (
|
||||
config: Record<string, unknown> = {},
|
||||
emitAndWait: EmitAndWait = async () => undefined,
|
||||
services: Record<string, unknown> = {},
|
||||
) => {
|
||||
const args = [
|
||||
{ env: 'prod', domain: 'puter.test', ...config },
|
||||
{ event: { emitAndWait: vi.fn(emitAndWait) } },
|
||||
{},
|
||||
{},
|
||||
services,
|
||||
] as unknown as ConstructorParameters<typeof PuterHomepageService>;
|
||||
return new PuterHomepageService(...args);
|
||||
};
|
||||
@@ -54,6 +55,7 @@ const render = async (
|
||||
req: Request = makeReq(),
|
||||
meta: Record<string, unknown> = { title: 'Puter' },
|
||||
launchOptions: Record<string, unknown> = {},
|
||||
actor: unknown = null,
|
||||
): Promise<string> => {
|
||||
let sent = '';
|
||||
const res = {
|
||||
@@ -61,7 +63,11 @@ const render = async (
|
||||
sent = html;
|
||||
},
|
||||
} as unknown as Response;
|
||||
await service.send({ req, res }, meta as never, launchOptions as never);
|
||||
await service.send(
|
||||
{ req, res, actor } as never,
|
||||
meta as never,
|
||||
launchOptions as never,
|
||||
);
|
||||
return sent;
|
||||
};
|
||||
|
||||
@@ -233,6 +239,40 @@ describe('PuterHomepageService — gui() parameters', () => {
|
||||
).toEqual({ login: true, signup: true });
|
||||
});
|
||||
|
||||
it('shows the teams UI to everyone with the deployment switch on', async () => {
|
||||
const on = makeService({ gui_params: { teams_ui: true } });
|
||||
expect(guiParamsOf(await render(on)).teams_ui).toBe(true);
|
||||
expect(guiParamsOf(await render(makeService())).teams_ui).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the teams UI per user on the domain allowlist, switch off', async () => {
|
||||
const teamsAvailableTo = vi.fn(async () => true);
|
||||
const service = makeService(
|
||||
{ teams_allowed_email_domains: ['puter.com'] },
|
||||
undefined,
|
||||
{ team: { teamsAvailableTo } },
|
||||
);
|
||||
|
||||
const staff = { user: { id: 7, email: 'j@puter.com' } };
|
||||
expect(
|
||||
guiParamsOf(await render(service, makeReq(), undefined, {}, staff))
|
||||
.teams_ui,
|
||||
).toBe(true);
|
||||
expect(teamsAvailableTo).toHaveBeenCalledWith(7, 'j@puter.com');
|
||||
|
||||
// Anonymous renders hide it; the API decides real access anyway.
|
||||
expect(guiParamsOf(await render(service)).teams_ui).toBe(false);
|
||||
|
||||
teamsAvailableTo.mockResolvedValueOnce(false);
|
||||
expect(
|
||||
guiParamsOf(
|
||||
await render(service, makeReq(), undefined, {}, {
|
||||
user: { id: 9, email: 'x@gmail.com' },
|
||||
}),
|
||||
).teams_ui,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('advertises notification events only with the fold-in switched on', async () => {
|
||||
expect(
|
||||
guiParamsOf(await render(makeService())).eventsNotifications,
|
||||
|
||||
@@ -230,8 +230,8 @@ export class PuterHomepageService extends PuterService {
|
||||
this.config.disable_user_signup ||
|
||||
this.config.gui_params?.disable_temp_users,
|
||||
),
|
||||
// Off until the teams UI ships; the API can be on without it.
|
||||
teams_ui: this.config.gui_params?.teams_ui === true,
|
||||
// Deployment-wide switch, or per user via the domain allowlist.
|
||||
teams_ui: await this.#teamsUiFor(actor),
|
||||
domain: this.config.domain,
|
||||
env,
|
||||
api_base_url: this.config.api_base_url,
|
||||
@@ -433,6 +433,27 @@ export class PuterHomepageService extends PuterService {
|
||||
</html>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The deployment-wide switch shows the teams UI to everyone; otherwise a
|
||||
* signed-in user on the domain allowlist (or already in a team) gets it.
|
||||
* The API gates real access either way — this only decides the render.
|
||||
*/
|
||||
async #teamsUiFor(actor: Actor | null): Promise<boolean> {
|
||||
if (this.config.gui_params?.teams_ui === true) return true;
|
||||
const domains = this.config.teams_allowed_email_domains;
|
||||
if (!Array.isArray(domains) || domains.length === 0) return false;
|
||||
const user = actor?.user;
|
||||
if (typeof user?.id !== 'number') return false;
|
||||
try {
|
||||
return await this.services.team.teamsAvailableTo(
|
||||
user.id,
|
||||
user.email ?? null,
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#originFromRequest(req: Request): string {
|
||||
// Prefer the pre-computed `config.origin` (protocol + domain + port).
|
||||
// Without it, non-80/443 deployments end up with URLs missing the
|
||||
|
||||
Reference in New Issue
Block a user