diff --git a/artifacts/api-server/src/routes/auth.ts b/artifacts/api-server/src/routes/auth.ts index 9bffb08f..1e174f75 100644 --- a/artifacts/api-server/src/routes/auth.ts +++ b/artifacts/api-server/src/routes/auth.ts @@ -6,6 +6,7 @@ import { usersTable, userRolesTable, rolesTable, + appSettingsTable, } from "@workspace/db"; import { requireAuth, getUserRoles } from "../middlewares/auth"; import { RegisterBody, LoginBody, UpdateLanguageBody } from "@workspace/api-zod"; @@ -34,6 +35,12 @@ router.post("/auth/register", async (req, res): Promise => { return; } + const [settings] = await db.select().from(appSettingsTable).where(eq(appSettingsTable.id, 1)); + if (settings && !settings.registrationOpen) { + res.status(403).json({ error: "Registration is closed" }); + return; + } + const { username, email, password, displayNameAr, displayNameEn, preferredLanguage } = parsed.data; const existing = await db diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 446a0a0d..53c2797f 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -17,6 +17,7 @@ "registerButton": "تسجيل", "welcomeBack": "مرحباً بعودتك", "createAccount": "حساب جديد", + "registrationClosed": "إنشاء الحسابات العامة مغلق حالياً. تواصل مع مدير النظام.", "displayNameAr": "الاسم (عربي)", "displayNameEn": "الاسم (إنجليزي)" }, @@ -93,7 +94,9 @@ "active": "نشط", "siteSettings": "إعدادات الموقع", "siteNameAr": "اسم الموقع (عربي)", - "siteNameEn": "اسم الموقع (إنجليزي)" + "siteNameEn": "اسم الموقع (إنجليزي)", + "registrationOpen": "السماح بإنشاء حسابات", + "registrationOpenHint": "عند الإيقاف، الأدمن فقط يقدر ينشئ مستخدمين جدد." }, "notFound": { "title": "404 — الصفحة غير موجودة", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index bb71d224..952fc4c6 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -17,6 +17,7 @@ "registerButton": "Register", "welcomeBack": "Welcome Back", "createAccount": "Create Account", + "registrationClosed": "Public registration is currently closed. Please contact the administrator.", "displayNameAr": "Name (Arabic)", "displayNameEn": "Name (English)" }, @@ -93,7 +94,9 @@ "active": "Active", "siteSettings": "Site Settings", "siteNameAr": "Site Name (Arabic)", - "siteNameEn": "Site Name (English)" + "siteNameEn": "Site Name (English)", + "registrationOpen": "Allow public registration", + "registrationOpenHint": "When off, only admins can create new users." }, "notFound": { "title": "404 — Page Not Found", diff --git a/artifacts/teaboy-os/src/pages/admin.tsx b/artifacts/teaboy-os/src/pages/admin.tsx index 41c3883b..8ab39685 100644 --- a/artifacts/teaboy-os/src/pages/admin.tsx +++ b/artifacts/teaboy-os/src/pages/admin.tsx @@ -508,10 +508,14 @@ function SiteSettingsPanel() { const queryClient = useQueryClient(); const { data } = useGetAppSettings({ query: { queryKey: getGetAppSettingsQueryKey() } }); const update = useUpdateAppSettings(); - const [form, setForm] = useState({ siteNameAr: "", siteNameEn: "" }); + const [form, setForm] = useState({ siteNameAr: "", siteNameEn: "", registrationOpen: true }); useEffect(() => { - if (data) setForm({ siteNameAr: data.siteNameAr, siteNameEn: data.siteNameEn }); + if (data) setForm({ + siteNameAr: data.siteNameAr, + siteNameEn: data.siteNameEn, + registrationOpen: data.registrationOpen, + }); }, [data]); const handleSave = () => { @@ -547,6 +551,16 @@ function SiteSettingsPanel() { dir="ltr" /> +
+
+ +

{t("admin.registrationOpenHint")}

+
+ setForm({ ...form, registrationOpen: v })} + /> +
diff --git a/artifacts/teaboy-os/src/pages/login.tsx b/artifacts/teaboy-os/src/pages/login.tsx index ecd1297c..873ff2dc 100644 --- a/artifacts/teaboy-os/src/pages/login.tsx +++ b/artifacts/teaboy-os/src/pages/login.tsx @@ -8,12 +8,13 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useToast } from "@/hooks/use-toast"; -import { useAppName } from "@/hooks/use-app-settings"; +import { useAppName, useAppSettings } from "@/hooks/use-app-settings"; import i18n from "@/i18n"; export default function LoginPage() { const { t } = useTranslation(); const appName = useAppName(); + const { data: settings } = useAppSettings(); const [, setLocation] = useLocation(); const { toast } = useToast(); const queryClient = useQueryClient(); @@ -86,14 +87,16 @@ export default function LoginPage() { -
- -
+ {settings?.registrationOpen && ( +
+ +
+ )}
+
+ + ); + } + return (
diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 7d9e0ee9..7091b7db 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -260,6 +260,7 @@ export interface Notification { export interface AppSettings { siteNameAr: string; siteNameEn: string; + registrationOpen: boolean; updatedAt?: string; } @@ -274,6 +275,7 @@ export interface UpdateAppSettingsBody { * @maxLength 200 */ siteNameEn?: string; + registrationOpen?: boolean; } export interface RequestUploadUrlBody { diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index f3da45d3..0ef9193f 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -1179,12 +1179,14 @@ components: AppSettings: type: object - required: [siteNameAr, siteNameEn] + required: [siteNameAr, siteNameEn, registrationOpen] properties: siteNameAr: type: string siteNameEn: type: string + registrationOpen: + type: boolean updatedAt: type: string format: date-time @@ -1200,6 +1202,8 @@ components: type: string minLength: 1 maxLength: 200 + registrationOpen: + type: boolean RequestUploadUrlBody: type: object diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index d23a6e14..bbce445a 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -203,6 +203,7 @@ export const DeleteAppParams = zod.object({ export const GetAppSettingsResponse = zod.object({ siteNameAr: zod.string(), siteNameEn: zod.string(), + registrationOpen: zod.boolean(), updatedAt: zod.coerce.date().optional(), }); @@ -224,11 +225,13 @@ export const UpdateAppSettingsBody = zod.object({ .min(1) .max(updateAppSettingsBodySiteNameEnMax) .optional(), + registrationOpen: zod.boolean().optional(), }); export const UpdateAppSettingsResponse = zod.object({ siteNameAr: zod.string(), siteNameEn: zod.string(), + registrationOpen: zod.boolean(), updatedAt: zod.coerce.date().optional(), }); diff --git a/lib/db/src/schema/settings.ts b/lib/db/src/schema/settings.ts index ebba87fc..4271a87b 100644 --- a/lib/db/src/schema/settings.ts +++ b/lib/db/src/schema/settings.ts @@ -1,4 +1,4 @@ -import { pgTable, varchar, integer, timestamp } from "drizzle-orm/pg-core"; +import { pgTable, varchar, integer, timestamp, boolean } from "drizzle-orm/pg-core"; import { createInsertSchema } from "drizzle-zod"; import { z } from "zod/v4"; @@ -6,6 +6,7 @@ export const appSettingsTable = pgTable("app_settings", { id: integer("id").primaryKey().default(1), siteNameAr: varchar("site_name_ar", { length: 200 }).notNull().default("نظام TeaBoy"), siteNameEn: varchar("site_name_en", { length: 200 }).notNull().default("TeaBoy OS"), + registrationOpen: boolean("registration_open").notNull().default(true), updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow().$onUpdate(() => new Date()), });