mirror of
https://github.com/fosrl/pangolin.git
synced 2025-08-28 13:48:13 +02:00
add tos and pp consent
This commit is contained in:
parent
f1bba3b958
commit
114ce8997f
5 changed files with 112 additions and 16 deletions
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue