add tos and pp consent

This commit is contained in:
miloschwartz 2025-07-21 16:56:47 -07:00
parent f1bba3b958
commit 114ce8997f
No known key found for this signature in database
5 changed files with 112 additions and 16 deletions

View file

@ -1274,5 +1274,11 @@
"createDomainDnsPropagation": "DNS Propagation", "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.", "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", "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"
}
} }

View file

@ -5,7 +5,8 @@ import {
boolean, boolean,
integer, integer,
bigint, bigint,
real real,
text
} from "drizzle-orm/pg-core"; } from "drizzle-orm/pg-core";
import { InferSelectModel } from "drizzle-orm"; import { InferSelectModel } from "drizzle-orm";
@ -135,6 +136,8 @@ export const users = pgTable("user", {
twoFactorSecret: varchar("twoFactorSecret"), twoFactorSecret: varchar("twoFactorSecret"),
emailVerified: boolean("emailVerified").notNull().default(false), emailVerified: boolean("emailVerified").notNull().default(false),
dateCreated: varchar("dateCreated").notNull(), dateCreated: varchar("dateCreated").notNull(),
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
termsVersion: varchar("termsVersion"),
serverAdmin: boolean("serverAdmin").notNull().default(false) serverAdmin: boolean("serverAdmin").notNull().default(false)
}); });

View file

@ -154,6 +154,8 @@ export const users = sqliteTable("user", {
.notNull() .notNull()
.default(false), .default(false),
dateCreated: text("dateCreated").notNull(), dateCreated: text("dateCreated").notNull(),
termsAcceptedTimestamp: text("termsAcceptedTimestamp"),
termsVersion: text("termsVersion"),
serverAdmin: integer("serverAdmin", { mode: "boolean" }) serverAdmin: integer("serverAdmin", { mode: "boolean" })
.notNull() .notNull()
.default(false) .default(false)

View file

@ -21,15 +21,14 @@ import { hashPassword } from "@server/auth/password";
import { checkValidInvite } from "@server/auth/checkValidInvite"; import { checkValidInvite } from "@server/auth/checkValidInvite";
import { passwordSchema } from "@server/auth/passwordSchema"; import { passwordSchema } from "@server/auth/passwordSchema";
import { UserType } from "@server/types/UserTypes"; import { UserType } from "@server/types/UserTypes";
import { build } from "@server/build";
export const signupBodySchema = z.object({ export const signupBodySchema = z.object({
email: z email: z.string().toLowerCase().email(),
.string()
.toLowerCase()
.email(),
password: passwordSchema, password: passwordSchema,
inviteToken: z.string().optional(), inviteToken: z.string().optional(),
inviteId: z.string().optional() inviteId: z.string().optional(),
termsAcceptedTimestamp: z.string().nullable().optional()
}); });
export type SignUpBody = z.infer<typeof signupBodySchema>; export type SignUpBody = z.infer<typeof signupBodySchema>;
@ -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 passwordHash = await hashPassword(password);
const userId = generateId(15); 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({ await db.insert(users).values({
userId: userId, userId: userId,
type: UserType.Internal, type: UserType.Internal,
username: email, username: email,
email: email, email: email,
passwordHash, passwordHash,
dateCreated: moment().toISOString() dateCreated: moment().toISOString(),
termsAcceptedTimestamp: termsAcceptedTimestamp || null,
termsVersion: "1"
}); });
// give the user their default permissions: // give the user their default permissions:

View file

@ -6,6 +6,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod"; import * as z from "zod";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { import {
Form, Form,
FormControl, FormControl,
@ -33,6 +34,7 @@ import Image from "next/image";
import { cleanRedirect } from "@app/lib/cleanRedirect"; import { cleanRedirect } from "@app/lib/cleanRedirect";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import BrandingLogo from "@app/components/BrandingLogo"; import BrandingLogo from "@app/components/BrandingLogo";
import { build } from "@server/build";
type SignupFormProps = { type SignupFormProps = {
redirect?: string; redirect?: string;
@ -44,7 +46,19 @@ const formSchema = z
.object({ .object({
email: z.string().email({ message: "Invalid email address" }), email: z.string().email({ message: "Invalid email address" }),
password: passwordSchema, 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, { .refine((data) => data.password === data.confirmPassword, {
path: ["confirmPassword"], path: ["confirmPassword"],
@ -64,13 +78,15 @@ export default function SignupForm({
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [termsAgreedAt, setTermsAgreedAt] = useState<string | null>(null);
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
defaultValues: { defaultValues: {
email: "", email: "",
password: "", password: "",
confirmPassword: "" confirmPassword: "",
agreeToTerms: false
} }
}); });
@ -85,7 +101,8 @@ export default function SignupForm({
email, email,
password, password,
inviteId, inviteId,
inviteToken inviteToken,
termsAcceptedTimestamp: termsAgreedAt
}) })
.catch((e) => { .catch((e) => {
console.error(e); console.error(e);
@ -120,14 +137,23 @@ export default function SignupForm({
return t("authCreateAccount"); 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 ( return (
<Card className="w-full max-w-md shadow-md"> <Card className="w-full max-w-md shadow-md">
<CardHeader className="border-b"> <CardHeader className="border-b">
<div className="flex flex-row items-center justify-center"> <div className="flex flex-row items-center justify-center">
<BrandingLogo <BrandingLogo height={58} width={175} />
height={58}
width={175}
/>
</div> </div>
<div className="text-center space-y-1 pt-3"> <div className="text-center space-y-1 pt-3">
<p className="text-muted-foreground">{getSubtitle()}</p> <p className="text-muted-foreground">{getSubtitle()}</p>
@ -180,6 +206,54 @@ export default function SignupForm({
</FormItem> </FormItem>
)} )}
/> />
{build === "saas" && (
<FormField
control={form.control}
name="agreeToTerms"
render={({ field }) => (
<FormItem className="flex flex-row items-center">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
handleTermsChange(
checked as boolean
);
}}
/>
</FormControl>
<div className="leading-none">
<FormLabel className="text-sm font-normal">
{t("signUpTerms.IAgreeToThe")}
<a
href="https://digpangolin.com/terms-of-service.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t(
"signUpTerms.termsOfService"
)}
</a>
{t("signUpTerms.and")}
<a
href="https://digpangolin.com/privacy-policy.html"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline"
>
{t(
"signUpTerms.privacyPolicy"
)}
</a>
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
)}
{error && ( {error && (
<Alert variant="destructive"> <Alert variant="destructive">