Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1889e7d
first bad pass
tefkah Mar 18, 2025
d62f2b5
chore: merge
tefkah Mar 20, 2025
827ca4c
feat: add invite table
tefkah Mar 20, 2025
95d6d81
refactor: split up the signup code somewhat
tefkah Mar 24, 2025
7e0ccdf
refactor: split up signup form into three forms
tefkah Mar 24, 2025
19304b2
refactor: move public signup form to different place, do some checks
tefkah Mar 24, 2025
437e87d
feat: add public join page
tefkah Mar 24, 2025
8ecd5aa
fix: export invites
tefkah Mar 25, 2025
a90dda3
feat: add public signup stuff
tefkah Mar 25, 2025
6b59061
chore: merge
tefkah Mar 25, 2025
36e152d
feat: creaet happy path for unauthed signup
tefkah Mar 25, 2025
dbee4c3
feat: add user to pub after creation
tefkah Mar 25, 2025
766df14
docs: add warning about foreign key issues
tefkah Mar 25, 2025
5fd5b8b
chore: add todo comment
tefkah Mar 26, 2025
7e50b0f
fix: set correct type for subitbuttons stageid
tefkah Mar 26, 2025
2cb474e
fix: set correct type for stageId
tefkah Mar 26, 2025
cb9d39d
fix: no justify between for button formbuilder form
tefkah Mar 26, 2025
0ee2557
fix: do not check authentication in public layout
tefkah Mar 26, 2025
741bc0d
refactor: move join page to community signup page and make it work
tefkah Mar 26, 2025
a57ed77
refactor: auto log signup errors
tefkah Mar 26, 2025
4c57700
dev: add a bunch of tests for the public form
tefkah Mar 26, 2025
04d34e2
chore: remove signup service experiment
tefkah Mar 27, 2025
0a65544
chore: remove next plugin bc it crashes vscode
tefkah Mar 27, 2025
20b83ab
dev: fix tests by adding formcontrols around inputs
tefkah Mar 27, 2025
9053549
fix: actually remove next plugin
tefkah Mar 27, 2025
dfed56c
fix: write some docs on how to deal with next's bullshit
tefkah Mar 27, 2025
dd433ab
feat: add ability to link to current pub and other places with markdo…
tefkah Mar 27, 2025
50bc7c6
dev: more tests!
tefkah Mar 27, 2025
4c19832
chore: update test
tefkah Mar 27, 2025
7e7f8a1
fix: use maybeWithTransaction instead of directly creating transactio…
tefkah Mar 27, 2025
7d99902
fix: remove superfluous test
tefkah Mar 27, 2025
8b86f1f
fix: add authentication and add nicer submit button
tefkah Mar 27, 2025
f642721
fix: remove uneccesary tests
tefkah Mar 27, 2025
3c87c16
fix: preserve redirect correctly
tefkah Mar 27, 2025
c05e26d
dev: add test for sign in behavior
tefkah Mar 27, 2025
1e62278
chore: slightly dry out test
tefkah Mar 27, 2025
efae5f4
fix: slight improvements
tefkah Mar 27, 2025
96720d3
fix: fix type error
tefkah Mar 27, 2025
d842f33
fix: give croccroc auto public form
tefkah Mar 27, 2025
8ceb127
feat: remove invite only access type
tefkah Mar 27, 2025
6b3d44a
fix: add data migration part
tefkah Mar 27, 2025
0c321a8
fix: center signup form a bit
tefkah Mar 27, 2025
fcd721f
fix: actually add top margin
tefkah Mar 27, 2025
78ab091
fix: grr forgot to save
tefkah Mar 27, 2025
0d97bd2
Merge branch 'main' into tfk/public-form-users
tefkah Mar 27, 2025
af18745
fix: remove invite table for now
tefkah Apr 1, 2025
343aeb8
Merge branch 'main' into tfk/public-form-users
tefkah Apr 1, 2025
ca84e1f
chore: merge
tefkah Apr 2, 2025
23aa0b0
Merge branch 'main' into tfk/public-form-users
tefkah Apr 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion config/tsconfig/nextjs.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this removal doesn't really do anything other than make me feel better.

"allowImportingTsExtensions": true,
"target": "es2018",
"lib": ["dom", "dom.iterable", "esnext"],
Expand Down
4 changes: 2 additions & 2 deletions core/app/(user)/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { AuthTokenType } from "db/public";

import { getLoginData } from "~/lib/authentication/loginData";
import { SignupForm } from "./SignupForm";
import { LegacySignupForm } from "../../components/Signup/LegacySignupForm";

export default async function Page() {
const { user, session } = await getLoginData({
Expand All @@ -19,7 +19,7 @@ export default async function Page() {

return (
<div className="m-auto max-w-lg">
<SignupForm user={user} />
<LegacySignupForm user={user} />
</div>
);
}
14 changes: 3 additions & 11 deletions core/app/c/(public)/[communitySlug]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Metadata } from "next";

import { notFound } from "next/navigation";

import { CommunityProvider } from "~/app/components/providers/CommunityProvider";
import { getLoginData } from "~/lib/authentication/loginData";
import { getCommunityRole } from "~/lib/authentication/roles";
Expand Down Expand Up @@ -30,20 +32,10 @@ export default async function MainLayout(props: Props) {

const { children } = props;

const { user } = await getLoginData();

const community = await findCommunityBySlug(params.communitySlug);

if (!community) {
return null;
}

const role = getCommunityRole(user, { slug: params.communitySlug });

// the user is logged in, but not a member of the community
// we should bar them from accessing the page
if (user && !role) {
return null;
return notFound();
}

return <CommunityProvider community={community}>{children}</CommunityProvider>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import { randomUUID } from "crypto";
import type { Metadata } from "next";
import type { ReactNode } from "react";

import { notFound } from "next/navigation";
import { notFound, redirect } from "next/navigation";

import type { Communities, PubsId } from "db/public";
import { ElementType, MemberRole } from "db/public";
import { ElementType, FormAccessType, MemberRole } from "db/public";
import { expect } from "utils";

import type { Form } from "~/lib/server/form";
Expand Down Expand Up @@ -164,6 +164,13 @@ export default async function FormPage(props: {
}

if (!user && !session) {
if (form.access === "public") {
// redirect user to signup/login
redirect(
`/c/${params.communitySlug}/public/signup?redirectTo=/c/${params.communitySlug}/public/forms/${params.formSlug}/fill`
);
}

const result = await handleFormToken({
params,
searchParams,
Expand All @@ -184,12 +191,18 @@ export default async function FormPage(props: {

const role = getCommunityRole(user, { slug: params.communitySlug });
if (!role) {
// user is not a member of the community, but is logged in, and the form is public
if (form.access === "public") {
redirect(
`/c/${params.communitySlug}/public/signup?redirectTo=/c/${params.communitySlug}/public/forms/${params.formSlug}/fill`
);
}
// TODO: show no access page
return notFound();
}

// all other roles always have access to the form
if (role === MemberRole.contributor) {
if (role === MemberRole.contributor && form.access !== FormAccessType.public) {
const memberHasAccessToForm = await userHasPermissionToForm({
formSlug: params.formSlug,
userId: user.id,
Expand Down
72 changes: 72 additions & 0 deletions core/app/c/(public)/[communitySlug]/public/signup/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { notFound, redirect, RedirectType, unstable_rethrow } from "next/navigation";

import { MemberRole } from "db/public";
import { logger } from "logger";

import { JoinCommunityForm } from "~/app/components/Signup/JoinCommunityForm";
import { PublicSignupForm } from "~/app/components/Signup/PublicSignupForm";
import { getLoginData } from "~/lib/authentication/loginData";
import { findCommunityBySlug } from "~/lib/server/community";
import { publicSignupsAllowed } from "~/lib/server/user";

export default async function Page({
params,
searchParams,
}: {
params: Promise<{ communitySlug: string }>;
searchParams: Promise<{ redirectTo?: string }>;
}) {
const [community, { user }] = await Promise.all([findCommunityBySlug(), getLoginData()]);

if (!community) {
logger.debug({
msg: "Community not found on signup page",
communitySlug: (await params).communitySlug,
});
notFound();
}

const isAllowedToSignup = await publicSignupsAllowed(community.id);

if (!isAllowedToSignup) {
// this community does not allow public signups
notFound();
}

const { redirectTo } = await searchParams;

if (user) {
if (user.memberships.some((m) => m.communityId === community.id)) {
redirect(redirectTo ?? `/c/${community.slug}/stages`);
// TODO: redirect to wherever they were redirected to before signing up
throw new Error("User is already member of community");
}

// TODO: figure this out based on the invite
const joinRole = MemberRole.contributor;

return (
<Wrapper>
<JoinCommunityForm community={community} role={joinRole} redirectTo={redirectTo} />
</Wrapper>
);
}

return (
<Wrapper>
<PublicSignupForm communityId={community.id} redirectTo={redirectTo} />
</Wrapper>
);
}

/**
* just a wrapper that centers stuff on the page.
* could be put in a layout later
*/
const Wrapper = ({ children }: { children: React.ReactNode }) => {
return (
<div className="m-auto mt-16 flex min-h-[50vh] max-w-lg flex-col items-center justify-center">
{children}
</div>
);
};
1 change: 1 addition & 0 deletions core/app/c/[communitySlug]/pubs/[pubId]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ export default async function Page(props: {
>
<div className="flex justify-center py-10">
<div className="max-w-prose flex-1">
{/** TODO: Add suspense */}
<PubEditor
searchParams={searchParams}
pubId={pub.id}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export const ButtonConfigurationForm = ({
e.stopPropagation(); //prevent submission from propagating to parent form
form.handleSubmit(onSubmit)(e);
}}
className="flex h-full flex-col justify-between gap-2 pt-2"
className="flex h-full flex-col gap-4 pt-2"
>
<ButtonOption label={labelValue} readOnly />
<FormField
Expand Down
10 changes: 2 additions & 8 deletions core/app/components/FormBuilder/ElementPanel/SelectAccess.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,17 @@ import { Contact, Lock, Users } from "ui/icon";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select";

const iconsAndCopy = {
[FormAccessType.inviteOnly]: {
Icon: Contact,
description: "Accessible via URL with tracked submissions",
name: "Invite Only",
help: "Community members & invited contributors can submit",
},
Comment on lines -11 to -16
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As per the extra work in #1051 , we now only have private and public forms

[FormAccessType.private]: {
Icon: Lock,
description: "Only accessible via Pub editor",
name: "Private",
help: "Only community members can create and edit",
help: "Only community members or invitees can create and edit",
},
[FormAccessType.public]: {
Icon: Users,
description: "Accessible via URL with untracked submissions",
name: "Public",
help: "Anyone with the link can submit",
help: "Anyone with the link can signup can submit. NOTE: this enables public signups to your community.",
},
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,63 +1,49 @@
"use client";

import type { Static } from "@sinclair/typebox";

import React, { useCallback, useMemo } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { useMemo } from "react";
import Link from "next/link";
import { useSearchParams } from "next/navigation";
import { typeboxResolver } from "@hookform/resolvers/typebox";
import { Type } from "@sinclair/typebox";
import { useForm } from "react-hook-form";
import { registerFormats } from "schemas";

import type { Users } from "db/public";
import { Button } from "ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "ui/card";
import { Form, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "ui/card";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "ui/form";
import { Input } from "ui/input";

import { signup } from "~/lib/authentication/actions";
import { isClientException, useServerAction } from "~/lib/serverActions";

registerFormats();

const formSchema = Type.Object({
firstName: Type.String(),
lastName: Type.String(),
email: Type.String({ format: "email" }),
password: Type.String({
minLength: 8,
maxLength: 72,
}),
});
import type { SignupFormSchema } from "./schema";
import { FormSubmitButton } from "../SubmitButton";
import { compiledSignupFormSchema } from "./schema";

export function SignupForm(props: {
user: Pick<Users, "firstName" | "lastName" | "email" | "id">;
export function BaseSignupForm(props: {
user: Pick<Users, "firstName" | "lastName" | "email" | "id"> | null;
onSubmit: (data: SignupFormSchema) => Promise<void>;
redirectTo?: string;
}) {
const runSignup = useServerAction(signup);
const searchParams = useSearchParams();

const redirectTo = props.redirectTo ?? searchParams.get("redirectTo");

const resolver = useMemo(() => typeboxResolver(formSchema), []);
const resolver = useMemo(() => typeboxResolver(compiledSignupFormSchema), []);

const form = useForm<Static<typeof formSchema>>({
const form = useForm<SignupFormSchema>({
resolver,
defaultValues: { ...props.user, lastName: props.user.lastName ?? undefined },
defaultValues: { ...(props?.user ?? {}), lastName: props.user?.lastName ?? undefined },
});

const searchParams = useSearchParams();

const handleSubmit = useCallback(async (data: Static<typeof formSchema>) => {
await runSignup({
id: props.user.id,
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
password: data.password,
redirect: searchParams.get("redirectTo"),
});
}, []);

return (
<Form {...form}>
<form onSubmit={form.handleSubmit(handleSubmit)}>
<form onSubmit={form.handleSubmit(props.onSubmit)}>
<Card className="mx-auto max-w-sm">
<CardHeader>
<CardTitle className="text-xl">Sign Up</CardTitle>
Expand All @@ -74,8 +60,9 @@ export function SignupForm(props: {
render={({ field }) => (
<FormItem>
<FormLabel>First name</FormLabel>

<Input {...field} placeholder="Max" required />
<FormControl>
<Input {...field} placeholder="Max" required />
</FormControl>
<FormMessage />
</FormItem>
)}
Expand All @@ -86,7 +73,9 @@ export function SignupForm(props: {
render={({ field }) => (
<FormItem>
<FormLabel>Last name</FormLabel>
<Input {...field} placeholder="Robinson" required />
<FormControl>
<Input {...field} placeholder="Robinson" required />
</FormControl>
<FormMessage />
</FormItem>
)}
Expand All @@ -102,12 +91,14 @@ export function SignupForm(props: {
If you change this, we will ask you to confirm your
email again.
</FormDescription>
<Input
{...field}
type="email"
placeholder="m@example.com"
required
/>
<FormControl>
<Input
{...field}
type="email"
placeholder="mail@example.com"
required
/>
</FormControl>
<FormMessage />
</FormItem>
)}
Expand All @@ -119,15 +110,19 @@ export function SignupForm(props: {
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<Input {...field} type="password" />
<FormControl>
<Input {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<Button type="submit" className="w-full">
Finish sign up
</Button>
<FormSubmitButton
formState={form.formState}
className="w-full"
idleText="Finish sign up"
/>
</div>
{/* <div className="mt-4 text-center text-sm">
Already have an account?{" "}
Expand All @@ -136,6 +131,16 @@ export function SignupForm(props: {
</Link>
</div> */}
</CardContent>
<CardFooter>
Or{" "}
<Link
href={`/login${redirectTo ? `?redirectTo=${redirectTo}` : ""}`}
className="mx-1 font-semibold underline"
>
sign in
</Link>{" "}
if you already have an account
</CardFooter>
</Card>
</form>
</Form>
Expand Down
Loading