From 3f5bd863c2f5f8bf0aea6d3df43acca6fce5b91b Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Wed, 24 Jun 2026 02:41:23 +0530 Subject: [PATCH 1/4] feat: disable public signup toggle and react sdk prop --- .../__tests__/routes.projects.storage.test.js | 1 + .../src/controllers/project.controller.js | 30 +++++++++++++++++++ apps/dashboard-api/src/routes/projects.js | 2 ++ .../src/controllers/userAuth.controller.js | 10 +++++++ apps/web-dashboard/src/pages/Auth.jsx | 28 +++++++++++++++++ packages/common/src/models/Project.js | 1 + .../urbackend-react/src/components/UrAuth.tsx | 8 +++-- 7 files changed, 78 insertions(+), 2 deletions(-) diff --git a/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js b/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js index b16ef5531..42a9d151f 100644 --- a/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js +++ b/apps/dashboard-api/src/__tests__/routes.projects.storage.test.js @@ -68,6 +68,7 @@ jest.mock('../controllers/project.controller', () => { analytics: jest.fn(ok), updateAllowedDomains: jest.fn(ok), toggleAuth: jest.fn(ok), + togglePublicSignup: jest.fn(ok), updateAuthProviders: jest.fn(ok), updateCollectionRls: jest.fn(ok), listMailTemplates: jest.fn(ok), diff --git a/apps/dashboard-api/src/controllers/project.controller.js b/apps/dashboard-api/src/controllers/project.controller.js index 695d3ff44..be35e37a3 100644 --- a/apps/dashboard-api/src/controllers/project.controller.js +++ b/apps/dashboard-api/src/controllers/project.controller.js @@ -2553,6 +2553,36 @@ module.exports.toggleAuth = async (req, res) => { } }; +module.exports.togglePublicSignup = async (req, res) => { + try { + const { projectId } = req.params; + const { enable } = req.body; + + const project = await Project.findOne({ + _id: projectId, + ...getProjectAccessQuery(req.user._id), + }); + if (!project) return res.status(404).json({ error: "Project not found" }); + + project.allowPublicSignup = !!enable; + await project.save(); + + await deleteProjectById(projectId); + await deleteProjectByApiKeyCache(project.publishableKey); + await deleteProjectByApiKeyCache(project.secretKey); + + const projectObj = sanitizeProjectResponse(project.toObject()); + + res.json({ + message: `Public signup ${project.allowPublicSignup ? "enabled" : "disabled"} successfully`, + allowPublicSignup: project.allowPublicSignup, + project: projectObj, + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}; + module.exports.updateAuthProviders = async (req, res) => { try { const { projectId } = req.params; diff --git a/apps/dashboard-api/src/routes/projects.js b/apps/dashboard-api/src/routes/projects.js index 45706716d..b66377c13 100644 --- a/apps/dashboard-api/src/routes/projects.js +++ b/apps/dashboard-api/src/routes/projects.js @@ -31,6 +31,7 @@ const { analytics, updateAllowedDomains, toggleAuth, + togglePublicSignup, updateAuthProviders, updateCollectionRls, listMailTemplates, @@ -138,6 +139,7 @@ router.get('/:projectId/analytics', authMiddleware, authorizeProject(), analytic // PATCH REQ FOR TOGGLE AUTH router.patch('/:projectId/auth/toggle', authMiddleware, authorizeProject('admin'), verifyEmail, toggleAuth); +router.patch('/:projectId/auth/public-signup', authMiddleware, authorizeProject('admin'), verifyEmail, togglePublicSignup); // PATCH REQ FOR SOCIAL AUTH PROVIDERS router.patch('/:projectId/auth/providers', authMiddleware, authorizeProject('admin'), planEnforcement.attachDeveloper, verifyEmail, planEnforcement.checkByokGate, updateAuthProviders); diff --git a/apps/public-api/src/controllers/userAuth.controller.js b/apps/public-api/src/controllers/userAuth.controller.js index 12eb13d13..5cd48dcd2 100644 --- a/apps/public-api/src/controllers/userAuth.controller.js +++ b/apps/public-api/src/controllers/userAuth.controller.js @@ -617,6 +617,12 @@ const findOrCreateSocialUser = async ({ project, usersColConfig, Model, provider return { user, isNewUser: false, linkedByEmail: true }; } + if (project.allowPublicSignup === false) { + const err = new Error("Public signup is disabled for this project. Only existing users can log in."); + err.statusCode = 403; + throw err; + } + const newUserPayload = await buildSocialAuthUserPayload(usersColConfig, profile); newUserPayload[providerIdField] = profile.providerUserId; newUserPayload.authProviders = [providerName]; @@ -988,6 +994,10 @@ module.exports.signup = async (req, res, next) => { try { const project = req.project; + if (project.allowPublicSignup === false) { + return next(new AppError(403, "Public signup is disabled for this project")); + } + const { email, password, username, ...otherData } = userSignupSchema.parse(req.body); const normalizedEmail = email.toLowerCase().trim(); diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx index 5970a5c68..d09a21fc6 100644 --- a/apps/web-dashboard/src/pages/Auth.jsx +++ b/apps/web-dashboard/src/pages/Auth.jsx @@ -148,6 +148,16 @@ export default function Auth() { finally { setIsSavingProviders(false); } }; + const handleTogglePublicSignup = async (enable) => { + try { + const res = await api.patch(`/api/projects/${projectId}/auth/public-signup`, { enable }); + setProject(res.data.project); + toast.success(`Public signup ${enable ? 'enabled' : 'disabled'}`); + } catch (err) { + toast.error(err.response?.data?.message || 'Failed to toggle public signup'); + } + }; + const goToUsersSchemaPreset = () => { navigate(`/project/${projectId}/create-collection?name=users&preset=auth-users`); }; @@ -356,6 +366,24 @@ export default function Auth() {
+ +
+
+

Allow Public Signups

+

+ When disabled, new users cannot sign up via API or OAuth. Only existing users can log in. +

+
+ +
+ setIsSocialAuthModalOpen(true)} diff --git a/packages/common/src/models/Project.js b/packages/common/src/models/Project.js index 80c3ec7bd..89bfe8d5b 100755 --- a/packages/common/src/models/Project.js +++ b/packages/common/src/models/Project.js @@ -107,6 +107,7 @@ const projectSchema = new mongoose.Schema( required: true, }, isAuthEnabled: { type: Boolean, default: false }, + allowPublicSignup: { type: Boolean, default: true }, siteUrl: { type: String, default: "" }, /** * Managed OAuth providers for project user authentication. diff --git a/sdks/urbackend-react/src/components/UrAuth.tsx b/sdks/urbackend-react/src/components/UrAuth.tsx index ea6754275..311ed73f4 100644 --- a/sdks/urbackend-react/src/components/UrAuth.tsx +++ b/sdks/urbackend-react/src/components/UrAuth.tsx @@ -80,6 +80,7 @@ export interface UrAuthProps { branding?: AuthBranding; labels?: Partial; onSuccess?: () => void; + hideSignup?: boolean; } const defaultLabels: AuthLabels = { @@ -153,10 +154,11 @@ export const UrAuth: React.FC = ({ colors, branding, labels, - onSuccess + onSuccess, + hideSignup = false }) => { const { login, signUp, socialLogin, requestPasswordReset, resetPassword, isLoading, error, clearError } = useAuth(); - const [mode, setMode] = useState<'signin' | 'signup' | 'forgot' | 'reset'>('signin'); + const [mode, setMode] = useState<'signin' | 'signup' | 'forgot' | 'reset'>(hideSignup ? 'signin' : 'signin'); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [otp, setOtp] = useState(''); @@ -194,6 +196,8 @@ export const UrAuth: React.FC = ({ const hasPasswordAuth = isEmailPasswordEnabled; const hasSocialAuth = isGoogleEnabled || isGithubEnabled; + + const showSwitcher = hasPasswordAuth && !hideSignup; const brandName = branding?.brandName || branding?.appName || branding?.title || 'urBackend'; const headerTitle = branding?.title || brandName; const brandingLogo = branding?.logo ?? branding?.logoUrl; From 171e279b6b7929c2002cbe974a035074e93b235f Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Wed, 24 Jun 2026 03:04:03 +0530 Subject: [PATCH 2/4] fix: address PR feedback on togglePublicSignup and React SDK UI --- .../src/controllers/project.controller.js | 21 ++++++++++++------ .../src/controllers/userAuth.controller.js | 4 +--- apps/web-dashboard/src/pages/Auth.jsx | 22 ++++++++++++------- .../urbackend-react/src/components/UrAuth.tsx | 3 +-- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/dashboard-api/src/controllers/project.controller.js b/apps/dashboard-api/src/controllers/project.controller.js index be35e37a3..37b0f8de2 100644 --- a/apps/dashboard-api/src/controllers/project.controller.js +++ b/apps/dashboard-api/src/controllers/project.controller.js @@ -2553,18 +2553,22 @@ module.exports.toggleAuth = async (req, res) => { } }; -module.exports.togglePublicSignup = async (req, res) => { +module.exports.togglePublicSignup = async (req, res, next) => { try { const { projectId } = req.params; const { enable } = req.body; + if (typeof enable !== "boolean") { + return next(new AppError(400, "enable parameter must be a boolean")); + } + const project = await Project.findOne({ _id: projectId, ...getProjectAccessQuery(req.user._id), }); - if (!project) return res.status(404).json({ error: "Project not found" }); + if (!project) return next(new AppError(404, "Project not found")); - project.allowPublicSignup = !!enable; + project.allowPublicSignup = enable; await project.save(); await deleteProjectById(projectId); @@ -2574,12 +2578,15 @@ module.exports.togglePublicSignup = async (req, res) => { const projectObj = sanitizeProjectResponse(project.toObject()); res.json({ - message: `Public signup ${project.allowPublicSignup ? "enabled" : "disabled"} successfully`, - allowPublicSignup: project.allowPublicSignup, - project: projectObj, + success: true, + data: { + allowPublicSignup: project.allowPublicSignup, + project: projectObj, + }, + message: `Public signup ${project.allowPublicSignup ? "enabled" : "disabled"} successfully` }); } catch (err) { - res.status(500).json({ error: err.message }); + next(err); } }; diff --git a/apps/public-api/src/controllers/userAuth.controller.js b/apps/public-api/src/controllers/userAuth.controller.js index 5cd48dcd2..bd53d850d 100644 --- a/apps/public-api/src/controllers/userAuth.controller.js +++ b/apps/public-api/src/controllers/userAuth.controller.js @@ -618,9 +618,7 @@ const findOrCreateSocialUser = async ({ project, usersColConfig, Model, provider } if (project.allowPublicSignup === false) { - const err = new Error("Public signup is disabled for this project. Only existing users can log in."); - err.statusCode = 403; - throw err; + throw new AppError(403, "Public signup is disabled for this project. Only existing users can log in."); } const newUserPayload = await buildSocialAuthUserPayload(usersColConfig, profile); diff --git a/apps/web-dashboard/src/pages/Auth.jsx b/apps/web-dashboard/src/pages/Auth.jsx index d09a21fc6..69c4a2d47 100644 --- a/apps/web-dashboard/src/pages/Auth.jsx +++ b/apps/web-dashboard/src/pages/Auth.jsx @@ -34,6 +34,7 @@ export default function Auth() { const [searchTerm, setSearchTerm] = useState(''); const [project, setProject] = useState(null); const [isEnabling, setIsEnabling] = useState(false); + const [isTogglingSignup, setIsTogglingSignup] = useState(false); const [isSavingProviders, setIsSavingProviders] = useState(false); const [isSocialAuthModalOpen, setIsSocialAuthModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); @@ -60,6 +61,13 @@ export default function Auth() { const githubCallbackUrl = `${PUBLIC_API_URL}/api/userAuth/social/github/callback`; const googleCallbackUrl = `${PUBLIC_API_URL}/api/userAuth/social/google/callback`; + const myMember = project?.members?.find(m => { + const memberId = typeof m.user === 'object' ? m.user?._id : m.user; + return memberId?.toString() === user?._id?.toString() || m.email === user?.email; + }); + const myRole = project?.owner?.toString() === user?._id?.toString() ? 'owner' : (myMember?.role || 'viewer'); + const isViewer = myRole === 'viewer'; + // Flow Logic const usersCollection = project?.collections?.find(c => c.name === 'users'); const hasUserCollection = !!usersCollection; @@ -149,12 +157,16 @@ export default function Auth() { }; const handleTogglePublicSignup = async (enable) => { + if (isTogglingSignup || isViewer) return; + setIsTogglingSignup(true); try { const res = await api.patch(`/api/projects/${projectId}/auth/public-signup`, { enable }); setProject(res.data.project); toast.success(`Public signup ${enable ? 'enabled' : 'disabled'}`); } catch (err) { toast.error(err.response?.data?.message || 'Failed to toggle public signup'); + } finally { + setIsTogglingSignup(false); } }; @@ -220,13 +232,6 @@ export default function Auth() { if (loading) return
; - const myMember = project?.members?.find(m => { - const memberId = typeof m.user === 'object' ? m.user?._id : m.user; - return memberId?.toString() === user?._id?.toString() || m.email === user?.email; - }); - const myRole = project?.owner?.toString() === user?._id?.toString() ? 'owner' : (myMember?.role || 'viewer'); - const isViewer = myRole === 'viewer'; - return (
handleTogglePublicSignup(e.target.checked)} + disabled={isTogglingSignup || isViewer} /> - +
diff --git a/sdks/urbackend-react/src/components/UrAuth.tsx b/sdks/urbackend-react/src/components/UrAuth.tsx index 311ed73f4..5790838c3 100644 --- a/sdks/urbackend-react/src/components/UrAuth.tsx +++ b/sdks/urbackend-react/src/components/UrAuth.tsx @@ -208,7 +208,6 @@ export const UrAuth: React.FC = ({ : mode === 'forgot' ? text.forgotTitle : text.resetTitle); - const showSwitcher = hasPasswordAuth; useEffect(() => { if (error) { @@ -655,7 +654,7 @@ export const UrAuth: React.FC = ({ {(mode === 'signin' || mode === 'signup') && renderSocialButtons()}
- {hasPasswordAuth && ( + {hasPasswordAuth && (!hideSignup || mode !== 'signin') && (
{footerPrompt}
} onRedirect={() => window.location.href = '/dashboard'}> - + ); } From 23e41226f8a7fd240894bcd09c0a90c34eefb8dc Mon Sep 17 00:00:00 2001 From: yash-pouranik Date: Wed, 24 Jun 2026 03:13:32 +0530 Subject: [PATCH 4/4] docs: update mintlify docs for allowPublicSignup and hideSignup --- mintlify/docs/guides/authentication.mdx | 9 +++++++++ mintlify/docs/react-sdk/ur-auth.mdx | 1 + 2 files changed, 10 insertions(+) diff --git a/mintlify/docs/guides/authentication.mdx b/mintlify/docs/guides/authentication.mdx index e154579f5..cae39b3f2 100644 --- a/mintlify/docs/guides/authentication.mdx +++ b/mintlify/docs/guides/authentication.mdx @@ -9,6 +9,15 @@ All auth endpoints require your publishable key (`pk_live_...`) in the `x-api-ke **Base URL:** `https://api.ub.bitbros.in` +## Disabling Public Signups + +By default, any user can create an account. To block new registrations: +1. Navigate to your project dashboard. +2. Go to the **Authentication** page. +3. Turn off the **Allow Public Signups** toggle. + +Once disabled, new signups will receive a `403 Forbidden` error, but existing users can still log in. + ## The `users` collection contract Before using authentication, create a collection named `users` in your project. It must include at least these two fields: diff --git a/mintlify/docs/react-sdk/ur-auth.mdx b/mintlify/docs/react-sdk/ur-auth.mdx index b7b9be221..62982e379 100644 --- a/mintlify/docs/react-sdk/ur-auth.mdx +++ b/mintlify/docs/react-sdk/ur-auth.mdx @@ -35,6 +35,7 @@ export default function LoginPage() { | `colors` | `Partial` | `undefined` | Overrides for auth widget theme colors. | | `branding` | `AuthBranding` | `undefined` | Branding/white-label configuration (logo, app name, subtitle, brand color). | | `labels` | `Partial` | `undefined` | Text/copy overrides for tabs, titles, buttons, and footer prompts. | +| `hideSignup` | `boolean` | `false` | Hides the sign up tab and sign up footer toggle when true. | | `onSuccess` | `() => void` | `undefined` | Called after successful sign-in or sign-up flow. | > All customization props are optional. If omitted, `` falls back to the same default behavior/UI style as v0.1.x.