mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-07 06:23:05 +00:00
fix: oidc toggle (#4494)
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
This commit is contained in:
1 parent
a110be0340
commit
48cd4ea355
9 files changed
+260
-9
No files matched your search
@@ -200,6 +200,16 @@ export type EventMap = {
|
||||
};
|
||||
|
||||
// ---- Auth / signup ----
|
||||
/**
|
||||
* Which signup methods are open, emitted via `emitAndWait` before every
|
||||
* signup and whenever the GUI reads the provider list. A listener sets
|
||||
* `oidcOnly` to refuse form and temp signups, leaving OIDC and team
|
||||
* provisioning open; `message` is what a refused form signup is told.
|
||||
*/
|
||||
'puter.signup.policy': {
|
||||
oidcOnly: boolean;
|
||||
message: string | null;
|
||||
};
|
||||
'puter.signup.validate': {
|
||||
allow: boolean;
|
||||
email?: string;
|
||||
@@ -1041,10 +1051,11 @@ export type EventKey = keyof EventMap & string;
|
||||
// Generates a wildcard for every non-final dot-separated prefix of K.
|
||||
export type WildcardPrefixes<K extends string> =
|
||||
K extends `${infer Head}.${infer Tail}`
|
||||
? | `${Head}.*`
|
||||
| (Tail extends `${string}.${string}`
|
||||
? `${Head}.${WildcardPrefixes<Tail>}`
|
||||
: never)
|
||||
?
|
||||
| `${Head}.*`
|
||||
| (Tail extends `${string}.${string}`
|
||||
? `${Head}.${WildcardPrefixes<Tail>}`
|
||||
: never)
|
||||
: never;
|
||||
|
||||
export type ListenKey = EventKey | WildcardPrefixes<EventKey>;
|
||||
@@ -1055,7 +1066,10 @@ export type MatchingEvents<P extends ListenKey> = P extends `${infer Prefix}.*`
|
||||
|
||||
/** Acked, not acked, no socket anywhere, or sockets only in a peer region. */
|
||||
export type NotifDeliveryOutcome =
|
||||
'received' | 'unreceived' | 'no-socket' | 'remote-only';
|
||||
| 'received'
|
||||
| 'unreceived'
|
||||
| 'no-socket'
|
||||
| 'remote-only';
|
||||
|
||||
/** One socket's receipt for a live notification. */
|
||||
export interface NotifDeliveryResponder {
|
||||
|
||||
@@ -244,7 +244,10 @@ describe('OIDCController GET /auth/oidc/providers', () => {
|
||||
await callRoute('get', '/auth/oidc/providers', makeReq({}), res);
|
||||
// We configured a single `custom` provider; the live service
|
||||
// walks the config and returns its id.
|
||||
expect(captured.body).toEqual({ providers: ['custom'] });
|
||||
expect(captured.body).toEqual({
|
||||
providers: ['custom'],
|
||||
signupOidcOnly: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -396,9 +396,12 @@ export class OIDCController extends PuterController {
|
||||
},
|
||||
},
|
||||
async (_req: Request, res: Response) => {
|
||||
const providers =
|
||||
await this.services.oidc.getEnabledProviderIds();
|
||||
res.json({ providers });
|
||||
const [providers, policy] = await Promise.all([
|
||||
this.services.oidc.getEnabledProviderIds(),
|
||||
this.services.signup.signupPolicy(),
|
||||
]);
|
||||
// The signup window drops its email form when this is set.
|
||||
res.json({ providers, signupOidcOnly: policy.oidcOnly });
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -122,6 +122,59 @@ describe('SignupService.runSignupGate', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('an OIDC-only policy refuses form and temp signups before the validate hook, and lets OIDC and team provisioning through', async () => {
|
||||
const oidcOnly = (_k: string, data: unknown) => {
|
||||
Object.assign(data as object, { oidcOnly: true });
|
||||
};
|
||||
server.clients.event.on('puter.signup.policy', oidcOnly);
|
||||
try {
|
||||
const gate = (input: {
|
||||
isTemp: boolean;
|
||||
source?: 'oidc' | 'team_provision';
|
||||
}) =>
|
||||
capture('puter.signup.validate', () =>
|
||||
server.services.signup.runSignupGate({
|
||||
req,
|
||||
data: {},
|
||||
email: `policy-${slug()}@example.com`,
|
||||
bonusCode: null,
|
||||
...input,
|
||||
}),
|
||||
);
|
||||
const form = await gate({ isTemp: false });
|
||||
const temp = await gate({ isTemp: true });
|
||||
const oidc = await gate({ isTemp: false, source: 'oidc' });
|
||||
const team = await gate({
|
||||
isTemp: false,
|
||||
source: 'team_provision',
|
||||
});
|
||||
|
||||
expect(form.result).toMatchObject({
|
||||
verdict: 'blocked',
|
||||
code: 'signup_oidc_only',
|
||||
});
|
||||
expect(temp.result).toEqual({
|
||||
verdict: 'no_temp_user',
|
||||
message: null,
|
||||
code: null,
|
||||
});
|
||||
expect(form.seen).toHaveLength(0);
|
||||
expect(temp.seen).toHaveLength(0);
|
||||
expect(oidc.result.verdict).toBe('allowed');
|
||||
expect(team.result.verdict).toBe('allowed');
|
||||
expect(await server.services.signup.signupPolicy()).toEqual({
|
||||
oidcOnly: true,
|
||||
message: null,
|
||||
});
|
||||
} finally {
|
||||
server.clients.event.off('puter.signup.policy', oidcOnly);
|
||||
}
|
||||
expect(await server.services.signup.signupPolicy()).toEqual({
|
||||
oidcOnly: false,
|
||||
message: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('refuses an unknown bonus code before the validate hook runs', async () => {
|
||||
const { result, seen } = await capture('puter.signup.validate', () =>
|
||||
server.services.signup.runSignupGate({
|
||||
|
||||
@@ -89,6 +89,9 @@ export interface SignupInput {
|
||||
export const signupClientIp = (req: Request): string | null =>
|
||||
req.ip || req.socket?.remoteAddress || null;
|
||||
|
||||
const OIDC_ONLY_MESSAGE =
|
||||
'New accounts can only be created with a sign-in provider right now.';
|
||||
|
||||
/**
|
||||
* Account creation for every signup route (form, temp, OIDC) and email
|
||||
* confirmation for every confirmation route. Callers parse requests and set
|
||||
@@ -147,6 +150,30 @@ export class SignupService extends PuterService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The signup methods extensions allow right now; a failing hook restricts
|
||||
* nothing.
|
||||
*/
|
||||
async signupPolicy(): Promise<{
|
||||
oidcOnly: boolean;
|
||||
message: string | null;
|
||||
}> {
|
||||
const policy = { oidcOnly: false, message: null as string | null };
|
||||
try {
|
||||
await this.clients.event?.emitAndWait(
|
||||
'puter.signup.policy',
|
||||
policy,
|
||||
{},
|
||||
);
|
||||
} catch (e) {
|
||||
console.warn('[signup] policy hook failed:', e);
|
||||
}
|
||||
return {
|
||||
oidcOnly: policy.oidcOnly === true,
|
||||
message: typeof policy.message === 'string' ? policy.message : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The extension-owned verdict on a signup: a dead bonus code is refused
|
||||
* first (before `puter.signup.validate` records the attempt), then the
|
||||
@@ -157,6 +184,23 @@ export class SignupService extends PuterService {
|
||||
const clientIp = signupClientIp(req);
|
||||
const fingerprint = input.fingerprint ?? null;
|
||||
|
||||
// Only form and temp signups carry no source; OIDC and team
|
||||
// provisioning are never restricted by the policy.
|
||||
if (!input.source) {
|
||||
const policy = await this.signupPolicy();
|
||||
if (policy.oidcOnly) {
|
||||
// A refused temp signup sends the GUI to its signup window,
|
||||
// which then offers only the providers.
|
||||
return isTemp
|
||||
? { verdict: 'no_temp_user', message: null, code: null }
|
||||
: {
|
||||
verdict: 'blocked',
|
||||
message: policy.message ?? OIDC_ONLY_MESSAGE,
|
||||
code: 'signup_oidc_only',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
bonusCode &&
|
||||
!(
|
||||
|
||||
@@ -68,6 +68,15 @@ function UIWindowSignup(options) {
|
||||
return new Promise(async (resolve) => {
|
||||
const internal_id = window.uuidv4();
|
||||
|
||||
let offersProviders = false;
|
||||
// Form signups are closed, so the providers are the only way in.
|
||||
const showOidcOnly = (el_window) => {
|
||||
$(el_window).find('.signup-form').hide();
|
||||
$(el_window).find('.signup-with-email-wrapper').hide();
|
||||
$(el_window).find('.oidc-providers-wrapper').show();
|
||||
$(el_window).find('.signup-oidc-only-notice').show();
|
||||
};
|
||||
|
||||
const logo_clickable =
|
||||
!!options.window_options?.cover_page && !window.embedded_in_popup;
|
||||
|
||||
@@ -149,6 +158,7 @@ function UIWindowSignup(options) {
|
||||
h += '</form>';
|
||||
h +=
|
||||
'<div class="oidc-providers-wrapper" style="display:none; padding: 10px 0;">';
|
||||
h += `<p class="signup-oidc-only-notice" style="display:none;">${i18n('signup_oidc_only')}</p>`;
|
||||
h += `<button type="button" class="oidc-google-btn oidc-btn button button-block button-normal" style="display:none; align-items:center; justify-content:center; gap:8px;"><img style="width: 20px; height: 20px;" src="data:image/webp;base64,UklGRu4GAABXRUJQVlA4WAoAAAAQAAAAXwAAXwAAQUxQSAUDAAABoATJtmnbGs+23+vZtm3btm3btnHxjG/btm3bnI197tljzjXjtSNiAnAjmbNs8x5DR40e2qtFhTzuVJ2e+rrE/ODKwsZe5J90n9CfXVw6vLEvifIH87OHVOK0mLxSLpSyd4vZhyuGkP+amL45j7lVYn6+rQpfSYDvFzO0QgIdYeZJCTbNRpnfJeDXLTSQsH/X6yiBj1TrIoEPgnZzCXwwtMtL4AOhnfP/wPpC/X0Jux/Uj4jySzsGVskJADkq9dn4JKEP1NuJ6gMDELvrrTF6QP9fjX0FQc2xLZme0E8Tfko20LOmZdAD+tWF/lVlqNb7I9IFBt+hpUD9CZGuMNhT2ONh8GJvWPyK1QZedhdyS7j5Bqkf3Kwo3A3w8yTneTgq3NyOdOZMhqPplM/h6Z+Urp6UF+ZX8HQGZZIrVylw9SvGnRqP33ev2QdSEwlzpIaYTlCOUtSLQpGujN/hRePIFMZDbgyOrGUcdGNe5CBjoRtbIimMMW4ciVxgDHQjxb0LkRTGGF8OMha6kRZZyzjkxrHIFMbDbmyPdGX84cbiSDmGFPNiZASU0V60SvAV4y4vyiS4yhCNSuXZxb5lIOEMyngFRSF+nqg85YsAOjNuSoQ/GdLJXhpjfgbplE/tCbN2Bp0pMtHaBAoy5khOY78yHkjiJOdZW3OEOSmJihxZaymPUPMlgTc40sfQy5RnkWx3kjQ3c0ioI5LClyRpZWSRcJF8T5aMNbFKuHti4B2WnDVwUcj541SnyVeVlCp/KeQ0xE6jiZzNppD1jNDzxcO/PJG9BUhF9gl/B4jtNEQeGBAv55gnRfFfUI+oiMhLOwZWyRnJXKbltLOfim4nDt5XMn0N5Jz/e/Eb6OW9KMtDcx8GQrOLB6ug2zG8fdBuENoB6Jf5PaiVMPlkQP1hdEUov5aH2QpfBXETTK+y91dnGM9/zdh2BFj2bkNn8iHMEqeN7MiJgMe+pPZwP4Sef9J9vL8vD8oMH6tOT309zo+P7BlcAs7mLNu8x9BRY4b1aVO9MG4cAQBWUDggwgMAABAVAJ0BKmAAYAA+bSyTRqQiIaEtVEyQgA2JbAC++hZa3jl94/Jj2rrA/Tvv9y7x+OzH+L5o/8p7APMA/TPpD+YD+bf4T9bPfW/mfqo/wHqAf0v/hdYB6AH7Aemp+5HwS/uN+6XtXdQB1N/Rj+m/RWYp6GESYsQNCm+TZ3BvtEgBK+isF38GQzHNMrw+tkbAm5lUM4rWqU9srOS+RuAUa8nFYoFrGH73Fzd++s+LPyEGAAD+Yg+gQRuJCapqteWj86DdkHmfHdS+mWVn7Ue8RiK5AaWCIe7RXaDKn0baNe80expR5tTBclgnd+SEzNZ6N3NL1suPhGwRqKzB5wNnWUXD7R0IkT5uIwUhTsO/1ucVEMKvTZRe4j7WwrA+e53P8J1/cdIvKtrtZv9AH5u1heZ1dGQELuxMxbQUnrw6IUh0YLBKEjNnR3JIsFNwkC+WLw4wkCMati73lcMjT6U2KIYsr0g5bPzSmo3ir9qRjtIqthpPIvuvYt8AbMewSj96NIqTM14M6ABtOaY87d8HOU7baFGGQjU1f8vf7SV5AjDrfW0VVzQcBez+PYiPApyQUAozEmZFKBDvX9ifFFacoulG9NCPDXa1mRPVwzrKuN3VfwUfv8Bx8QJ88prsOCDd8UvNDnUzbRqBXOmQ4co+/xYy/iQV89GHs4pb0D2LYwv6aY8yPLCrYyAPUe4dKEh/FBqRBJ/716CJVMTCrvB+gSgPwNjN5LlnNQqkBsHnSqdUFddjvBKNdm6zm8ggLFJl14S37sJW1fAEbiB0IqxcqlJOWol+ecJy9xdESchmlROmo/4/9V34VN2cw8NUB5POPUNzivyMFfgF27QTFXgya/LF3OsfHMF6Itejgy4ab38VPFjez5f8Z/b/dhYV21SflE4KtUj8LZDrQXRp+Pzdf/gWfvm2oLh0x+Fr+ggo2jjG3DFn95Wj1P3ra1RtzYE8ffXBeR2lDhL+GPJiiZaVinGMkjVYup0AghAhIfVgFtsL8lhMyJY84ccksHz92o1/J3NfrGSM8NwOv7ieeBjq2eqCOmrUHpG3mlr8Hj/fR4d+orLlpipLTh3+cRY7suXQXw9SPYZieoKS/JE9y0f+BcWCzdNshWl2oVpeK2qm36iFltfGZUvXdfBKufE9RkZcyOBDUVeONlBchjuaLPNd6fSlk41Pp57nF5MmES2Kl7HqY+f2pvS71eIgUXpqtAFIY94JSmNre+g49tL8VnZlJNqgVhj/De//5zH/84h//5uJfunlawY99GgDkm8cYFbIAAAA" />${i18n('sign_up_with_google')}</button>`;
|
||||
h += `<button type="button" class="oidc-microsoft-btn oidc-btn button button-block button-normal" style="display:none; align-items:center; justify-content:center; gap:8px; margin-top:8px;"><svg style="width:20px; height:20px;" viewBox="0 0 23 23" fill="none"><rect x="1" y="1" width="10" height="10" fill="#f25022"/><rect x="12" y="1" width="10" height="10" fill="#7fba00"/><rect x="1" y="12" width="10" height="10" fill="#00a4ef"/><rect x="12" y="12" width="10" height="10" fill="#ffb900"/></svg>${i18n('sign_up_with_microsoft')}</button>`;
|
||||
h += `<button type="button" class="oidc-apple-btn oidc-btn button button-block button-normal" style="display:none; align-items:center; justify-content:center; gap:8px; margin-top:8px;"><svg style="width:20px; height:20px;" viewBox="0 0 384 512" fill="currentColor"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 4 184.8 4 273.5q0 39.3 14.4 81.2c12.8 36.7 59 126.7 107.2 125.2 25.2-.6 43-17.9 75.8-17.9 31.8 0 48.3 17.9 76.4 17.9 48.6-.7 90.4-82.5 102.6-119.3-65.2-30.7-61.7-90-61.7-91.9zm-56.6-164.2c27.3-32.4 24.8-61.9 24-72.5-24.1 1.4-52 16.4-67.9 34.9-17.5 19.8-27.8 44.3-25.6 71.9 26.1 2 49.9-11.4 69.5-34.3z"/></svg>${i18n('sign_up_with_apple')}</button>`;
|
||||
@@ -372,6 +382,7 @@ function UIWindowSignup(options) {
|
||||
});
|
||||
|
||||
if (hasProvider) {
|
||||
offersProviders = true;
|
||||
$(el_window).find('.oidc-providers-wrapper').show();
|
||||
// Hide the signup form and show the "Sign up with email" button
|
||||
$(el_window).find('.signup-form').hide();
|
||||
@@ -395,6 +406,10 @@ function UIWindowSignup(options) {
|
||||
?.focus({ preventScroll: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (data.signupOidcOnly === true && hasProvider) {
|
||||
showOidcOnly(el_window);
|
||||
}
|
||||
} catch (_) {}
|
||||
})();
|
||||
},
|
||||
@@ -743,6 +758,17 @@ function UIWindowSignup(options) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Form signups closed after this window loaded.
|
||||
// Without provider buttons (some popups), the
|
||||
// server's message below says it instead.
|
||||
if (
|
||||
errorJson?.code === 'signup_oidc_only' &&
|
||||
offersProviders
|
||||
) {
|
||||
showOidcOnly(el_window);
|
||||
return;
|
||||
}
|
||||
|
||||
// Drop the code so the next attempt signs up without it.
|
||||
if (errorJson?.code === 'bonus_code_invalid') {
|
||||
window.signup_bonus_code = null;
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
|
||||
|
||||
const state = vi.hoisted(() => ({ el: null }));
|
||||
vi.mock('./UIWindowLogin.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UIWindowEmailConfirmationRequired.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UIWindowPhoneVerificationRequired.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('./UIWindowCardVerificationRequired.js', () => ({ default: vi.fn() }));
|
||||
vi.mock('../helpers/authLogoHeader.js', () => ({
|
||||
authLogoHeader: () => '',
|
||||
wireAuthLogoHeader: vi.fn(),
|
||||
}));
|
||||
vi.mock('../helpers/checkPasswordStrength.js', () => ({
|
||||
default: () => ({ overallPass: true }),
|
||||
}));
|
||||
vi.mock('./UIWindow.js', () => ({
|
||||
default: async (options) => {
|
||||
state.el = document.createElement('div');
|
||||
state.el.innerHTML = options.body_content;
|
||||
document.body.append(state.el);
|
||||
options.onAppend?.(state.el);
|
||||
return state.el;
|
||||
},
|
||||
}));
|
||||
globalThis.def = globalThis.def ?? ((value) => value);
|
||||
const { default: UIWindowSignup } = await import('./UIWindowSignup.js');
|
||||
|
||||
const providers = (body) => {
|
||||
globalThis.fetch = vi.fn(async () => new Response(JSON.stringify(body)));
|
||||
};
|
||||
const find = (selector) => jQuery(state.el).find(selector);
|
||||
const shown = (selector) => find(selector).css('display') !== 'none';
|
||||
|
||||
const open = async () => {
|
||||
void UIWindowSignup({ reload_on_success: false, redirect_url: '/' });
|
||||
await vi.waitFor(() => expect(find('.oidc-google-btn').css('display')).toBe('flex'));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
globalThis.$ = jQuery;
|
||||
jQuery.fn.close = vi.fn();
|
||||
globalThis.i18n = (key) => key;
|
||||
globalThis.html_encode = (value) => jQuery('<div>').text(value).html();
|
||||
window.gui_origin = 'https://puter.test';
|
||||
window.api_origin = 'https://api.puter.test';
|
||||
window.gui_params = {};
|
||||
window.icons = {};
|
||||
window.uuidv4 = () => 'oidc-only-test';
|
||||
window.is_email = () => true;
|
||||
window.embedded_in_popup = false;
|
||||
window.url_query_params = new URLSearchParams('');
|
||||
state.el = null;
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('signup window when only OIDC signups are open', () => {
|
||||
it('offers the email form behind a button normally', async () => {
|
||||
providers({ providers: ['google'], signupOidcOnly: false });
|
||||
await open();
|
||||
|
||||
expect(shown('.signup-with-email-wrapper')).toBe(true);
|
||||
expect(shown('.signup-oidc-only-notice')).toBe(false);
|
||||
});
|
||||
|
||||
it('drops the email form and says why', async () => {
|
||||
providers({ providers: ['google'], signupOidcOnly: true });
|
||||
await open();
|
||||
|
||||
await vi.waitFor(() => expect(shown('.signup-oidc-only-notice')).toBe(true));
|
||||
expect(shown('.signup-form')).toBe(false);
|
||||
expect(shown('.signup-with-email-wrapper')).toBe(false);
|
||||
});
|
||||
|
||||
it('switches to the providers when a form signup is refused for it', async () => {
|
||||
providers({ providers: ['google'], signupOidcOnly: false });
|
||||
await open();
|
||||
jQuery.ajax = vi.fn((opts) =>
|
||||
opts.error({
|
||||
responseText: JSON.stringify({
|
||||
code: 'signup_oidc_only',
|
||||
message: 'closed',
|
||||
}),
|
||||
}));
|
||||
|
||||
find('.signup-with-email-btn').trigger('click');
|
||||
find('.username').val('someone');
|
||||
find('.email').val('someone@example.com');
|
||||
find('.password').val('a-Strong-pass-1');
|
||||
find('.confirm-password').val('a-Strong-pass-1');
|
||||
find('.signup-btn').trigger('click');
|
||||
|
||||
await vi.waitFor(() => expect(shown('.signup-oidc-only-notice')).toBe(true));
|
||||
expect(shown('.signup-form')).toBe(false);
|
||||
expect(shown('.signup-with-email-wrapper')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2260,6 +2260,13 @@ label {
|
||||
color: #611a15;
|
||||
}
|
||||
|
||||
.signup-oidc-only-notice {
|
||||
text-align: center;
|
||||
margin: 0 0 10px;
|
||||
font-size: 14px;
|
||||
color: #5f6368;
|
||||
}
|
||||
|
||||
/* -- Logo strip above the login / signup forms -- */
|
||||
.auth-logo-header {
|
||||
display: flex;
|
||||
|
||||
@@ -989,6 +989,7 @@ const en = {
|
||||
sign_in_with_provider: 'Sign in with %%',
|
||||
sign_up_with_provider: 'Sign up with %%',
|
||||
sign_up_with_email: 'Sign up using email',
|
||||
signup_oidc_only: 'New accounts can only be created with one of the options below.',
|
||||
popup_opener_uses_puter: '%strong% is powered by Puter for its AI and cloud features.\nUse your Puter account to continue.',
|
||||
popup_opener_origin_unsupported: 'Puter cannot sign you in to this page because the browser gives it no identifiable address. Pages opened straight from a file (file://) and iframes sandboxed without allow-same-origin are not supported — serve the page over http://localhost or a real domain and try again.',
|
||||
oidc_switched_to_login_message: 'You have been logged in to an existing account.',
|
||||
|
||||
Reference in new issue
Block a user