diff --git a/messages/en-US.json b/messages/en-US.json index 0c3dd233..ed004d99 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -1274,5 +1274,11 @@ "createDomainDnsPropagation": "DNS Propagation", "createDomainDnsPropagationDescription": "DNS changes may take some time to propagate across the internet. This can take anywhere from a few minutes to 48 hours, depending on your DNS provider and TTL settings.", "resourcePortRequired": "Port number is required for non-HTTP resources", - "resourcePortNotAllowed": "Port number should not be set for HTTP resources" + "resourcePortNotAllowed": "Port number should not be set for HTTP resources", + "signUpTerms": { + "IAgreeToThe": "I agree to the", + "termsOfService": "terms of service", + "and": "and", + "privacyPolicy": "privacy policy" + } } diff --git a/server/db/pg/schema.ts b/server/db/pg/schema.ts index e256f28d..77be5f1b 100644 --- a/server/db/pg/schema.ts +++ b/server/db/pg/schema.ts @@ -5,7 +5,8 @@ import { boolean, integer, bigint, - real + real, + text } from "drizzle-orm/pg-core"; import { InferSelectModel } from "drizzle-orm"; @@ -135,6 +136,8 @@ export const users = pgTable("user", { twoFactorSecret: varchar("twoFactorSecret"), emailVerified: boolean("emailVerified").notNull().default(false), dateCreated: varchar("dateCreated").notNull(), + termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"), + termsVersion: varchar("termsVersion"), serverAdmin: boolean("serverAdmin").notNull().default(false) }); diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema.ts index 3e442d07..2c44b593 100644 --- a/server/db/sqlite/schema.ts +++ b/server/db/sqlite/schema.ts @@ -154,6 +154,8 @@ export const users = sqliteTable("user", { .notNull() .default(false), dateCreated: text("dateCreated").notNull(), + termsAcceptedTimestamp: text("termsAcceptedTimestamp"), + termsVersion: text("termsVersion"), serverAdmin: integer("serverAdmin", { mode: "boolean" }) .notNull() .default(false) diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index 2508ecfe..09c8db07 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -21,15 +21,14 @@ import { hashPassword } from "@server/auth/password"; import { checkValidInvite } from "@server/auth/checkValidInvite"; import { passwordSchema } from "@server/auth/passwordSchema"; import { UserType } from "@server/types/UserTypes"; +import { build } from "@server/build"; export const signupBodySchema = z.object({ - email: z - .string() - .toLowerCase() - .email(), + email: z.string().toLowerCase().email(), password: passwordSchema, inviteToken: z.string().optional(), - inviteId: z.string().optional() + inviteId: z.string().optional(), + termsAcceptedTimestamp: z.string().nullable().optional() }); export type SignUpBody = z.infer; @@ -54,7 +53,8 @@ export async function signup( ); } - const { email, password, inviteToken, inviteId } = parsedBody.data; + const { email, password, inviteToken, inviteId, termsAcceptedTimestamp } = + parsedBody.data; const passwordHash = await hashPassword(password); const userId = generateId(15); @@ -161,13 +161,24 @@ export async function signup( } } + if (build === "saas" && !termsAcceptedTimestamp) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "You must accept the terms of service and privacy policy" + ) + ); + } + await db.insert(users).values({ userId: userId, type: UserType.Internal, username: email, email: email, passwordHash, - dateCreated: moment().toISOString() + dateCreated: moment().toISOString(), + termsAcceptedTimestamp: termsAcceptedTimestamp || null, + termsVersion: "1" }); // give the user their default permissions: diff --git a/src/app/auth/signup/SignupForm.tsx b/src/app/auth/signup/SignupForm.tsx index c6ed500b..5494ba10 100644 --- a/src/app/auth/signup/SignupForm.tsx +++ b/src/app/auth/signup/SignupForm.tsx @@ -6,6 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Checkbox } from "@/components/ui/checkbox"; import { Form, FormControl, @@ -33,6 +34,7 @@ import Image from "next/image"; import { cleanRedirect } from "@app/lib/cleanRedirect"; import { useTranslations } from "next-intl"; import BrandingLogo from "@app/components/BrandingLogo"; +import { build } from "@server/build"; type SignupFormProps = { redirect?: string; @@ -44,7 +46,19 @@ const formSchema = z .object({ email: z.string().email({ message: "Invalid email address" }), password: passwordSchema, - confirmPassword: passwordSchema + confirmPassword: passwordSchema, + agreeToTerms: z.boolean().refine( + (val) => { + if (build === "saas") { + val === true; + } + return true; + }, + { + message: + "You must agree to the terms of service and privacy policy" + } + ) }) .refine((data) => data.password === data.confirmPassword, { path: ["confirmPassword"], @@ -64,13 +78,15 @@ export default function SignupForm({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [termsAgreedAt, setTermsAgreedAt] = useState(null); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { email: "", password: "", - confirmPassword: "" + confirmPassword: "", + agreeToTerms: false } }); @@ -85,7 +101,8 @@ export default function SignupForm({ email, password, inviteId, - inviteToken + inviteToken, + termsAcceptedTimestamp: termsAgreedAt }) .catch((e) => { console.error(e); @@ -120,14 +137,23 @@ export default function SignupForm({ return t("authCreateAccount"); } + const handleTermsChange = (checked: boolean) => { + if (checked) { + const isoNow = new Date().toISOString(); + console.log("Terms agreed at:", isoNow); + setTermsAgreedAt(isoNow); + form.setValue("agreeToTerms", true); + } else { + form.setValue("agreeToTerms", false); + setTermsAgreedAt(null); + } + }; + return (
- +

{getSubtitle()}

@@ -180,6 +206,54 @@ export default function SignupForm({ )} /> + {build === "saas" && ( + ( + + + { + field.onChange(checked); + handleTermsChange( + checked as boolean + ); + }} + /> + +
+ + {t("signUpTerms.IAgreeToThe")} + + {t( + "signUpTerms.termsOfService" + )} + + {t("signUpTerms.and")} + + {t( + "signUpTerms.privacyPolicy" + )} + + + +
+
+ )} + /> + )} {error && (