Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ params: - api key - list of fields?
- template (should be able to reference user fields which will be substituted with values for the recipient. and pubs too, if that pub is in the instance scope)
- user vars:
- user.id
- user.name
- user.firstName
- user.lastName
- user.email
- user.JWT (useful for generating magic link)
- instance vars:
Expand Down
104 changes: 60 additions & 44 deletions core/app/(user)/settings/SettingsForm.tsx
Original file line number Diff line number Diff line change
@@ -1,69 +1,74 @@
"use client";
import React, { useState, FormEvent } from "react";
import { Button } from "ui";
import { UserPutBody } from "app/api/user/route";
import { supabase } from "lib/supabase";
import { useRouter } from "next/navigation";
import { getSlugSuffix, slugifyString } from "lib/string";
type Props = {
name: string;
email: string;
slug: string;
};
import { UserPutBody, UserSettings } from "~/lib/types";

export default function SettingsForm({ name: initName, email: initEmail, slug }: Props) {
const [name, setName] = useState(initName);
type Props = UserSettings;

export default function SettingsForm({
firstName: initFirstName,
lastName: initLastName,
email: initEmail,
slug,
}: Props) {
const [firstName, setFirstName] = useState(initFirstName);
const [lastName, setLastName] = useState(initLastName);
const [email, setEmail] = useState(initEmail);
const [emailError, setEmailError] = useState('')
const [emailError, setEmailError] = useState("");
const [emailIsLoading, setEmailIsLoading] = useState(false);
const [emailSuccess, setEmailSuccess] = useState(false)
const [isLoading, setIsLoading] = useState(false);
const [resetIsLoading, setResetIsLoading] = useState(false);
const [emailSuccess, setEmailSuccess] = useState(false);
const [, setIsLoading] = useState(false);
const [, setResetIsLoading] = useState(false);
const [resetSuccess, setResetSuccess] = useState(false);
const emailChanged = initEmail !== email;
const router = useRouter();
const valuesChanged = name !== initName;
const valuesChanged = emailChanged || firstName !== initFirstName || lastName !== initLastName;
const slugSuffix = getSlugSuffix(slug);

const updateEmail = async (e: FormEvent<EventTarget>) => {
e.preventDefault();
setEmailError("")
setEmailError("");
if (emailChanged) {
setEmailIsLoading(true);
const response = await fetch("/api/user?email=" + email, {
method: "GET",
headers: { "content-type": "application/json" },
})
const genericError = () => setEmailError('An error happened while trying to update your email')
});
const genericError = () =>
setEmailError("An error happened while trying to update your email");

if (!response.ok) {
if (response.status === 403) {
setEmailError(`A PubPub account already exists for ${email}`);
} else {
genericError()
const { message }: { message?: string } = await response.json()
genericError();
const { message }: { message?: string } = await response.json();
console.error(`Error: ${response.status} ${message}`);
}
setEmailIsLoading(false)
return
setEmailIsLoading(false);
return;
}

const { error } = await supabase.auth.updateUser({ email });
setEmailIsLoading(false);
if (error) {
genericError()
genericError();
console.error(error);
}
setEmailSuccess(true);
}
}
};

const handleSubmit = async (evt: FormEvent<EventTarget>) => {
evt.preventDefault();

setIsLoading(true);
let putBody: UserPutBody = {
name,
firstName,
lastName,
};
const response = await fetch("/api/user", {
method: "PUT",
Expand All @@ -74,7 +79,7 @@ export default function SettingsForm({ name: initName, email: initEmail, slug }:
setIsLoading(false);
if (!response.ok) {
if (data.message) {
console.error(data.message)
console.error(data.message);
}
} else {
router.refresh();
Expand All @@ -97,16 +102,26 @@ export default function SettingsForm({ name: initName, email: initEmail, slug }:
<>
<div className="my-10">
<form onSubmit={handleSubmit}>
<label htmlFor="name">Name</label>
<input name="name" value={name} onChange={(evt) => setName(evt.target.value)} />
<div className="text-gray-500 text-sm leading-tight">
Username: {slugifyString(name)}-{slugSuffix}
<div className="flex flex-row">
<label htmlFor="firstName">First Name</label>
<input
name="firstName"
value={firstName}
onChange={(evt) => setFirstName(evt.target.value)}
className="mr-2"
/>
<label htmlFor="lastName">Last Name</label>
<input
name="lastName"
value={lastName ?? ""}
onChange={(evt) => setLastName(evt.target.value)}
/>
</div>
<Button
className="mt-4"
type="submit"
disabled={!valuesChanged || !name}
>
<div className="text-gray-500 text-sm leading-tight mt-3">
Username: {slugifyString(firstName)}
{lastName ? `-${slugifyString(lastName)}` : ""}-{slugSuffix}
</div>
<Button className="mt-4" type="submit" disabled={!valuesChanged || !firstName}>
Save Changes
</Button>
</form>
Expand All @@ -120,20 +135,21 @@ export default function SettingsForm({ name: initName, email: initEmail, slug }:
/>
<Button
type="submit"
disabled={!email || !emailChanged || emailSuccess}
disabled={!email || !emailChanged || emailSuccess || !firstName}
>
Update Email
</Button>
{!emailIsLoading && (emailError ? (
<div className="text-red-700 text-sm leading-tight">
{emailError}
</div>
) : emailSuccess && (
<div className="text-red-700 text-sm leading-tight">
You will need to confirm this change by clicking a link sent to the new
email address.
</div>
))}
{!emailIsLoading &&
(emailError ? (
<div className="text-red-700 text-sm leading-tight">{emailError}</div>
) : (
emailSuccess && (
<div className="text-red-700 text-sm leading-tight">
You will need to confirm this change by clicking a link sent to
the new email address.
</div>
)
))}
</form>
<p className="my-4">
Click below to receive an email with a secure link for reseting yor password.
Expand Down
7 changes: 6 additions & 1 deletion core/app/(user)/settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ export default async function Page() {
}
return (
<div className="max-w-lg m-auto">
<SettingsForm name={loginData.name} email={loginData.email} slug={loginData.slug} />
<SettingsForm
firstName={loginData.firstName}
lastName={loginData.lastName}
email={loginData.email}
slug={loginData.slug}
/>
</div>
);
}
32 changes: 23 additions & 9 deletions core/app/(user)/signup/SignupForm.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";
import React, { useState, FormEvent } from "react";
import { Button } from "ui";
import { UserPostBody } from "app/api/user/route";
import { UserPostBody } from "~/lib/types";

export default function SignupForm() {
const [name, setName] = useState("");
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [password, setPassword] = useState("");
const [email, setEmail] = useState("");
const [isLoading, setIsLoading] = useState(false);
Expand All @@ -15,7 +16,8 @@ export default function SignupForm() {

setIsLoading(true);
const postBody: UserPostBody = {
name,
firstName,
lastName,
password,
email,
};
Expand All @@ -42,15 +44,27 @@ export default function SignupForm() {
<div className="my-10">
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<label htmlFor="firstName">Name</label>
</div>
<div>
<input
id="name"
id="firstName"
className="w-full"
name="name"
value={name}
onChange={(evt) => setName(evt.target.value)}
name="firstName"
value={firstName}
onChange={(evt) => setFirstName(evt.target.value)}
/>
</div>
<div>
<label htmlFor="lastName">Name</label>
</div>
<div>
<input
id="lastName"
className="w-full"
name="lastName"
value={lastName}
onChange={(evt) => setLastName(evt.target.value)}
/>
</div>
<div className="mt-2">
Expand Down Expand Up @@ -82,7 +96,7 @@ export default function SignupForm() {
<Button
variant="outline"
type="submit"
disabled={!name || !email || !password}
disabled={!firstName || !email || !password}
>
Create Account
</Button>
Expand Down
48 changes: 27 additions & 21 deletions core/app/api/user/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ import { getLoginId } from "lib/auth/loginId";
import { BadRequestError, ForbiddenError, UnauthorizedError, handleErrors } from "~/lib/server";

export type UserPostBody = {
name: string;
firstName: string;
lastName?: string;
email: string;
password: string;
};

export type UserPutBody = {
name: string;
firstName: string;
lastName?: string;
};

export async function POST(req: NextRequest) {
return await handleErrors(async () => {
const submittedData: UserPostBody = await req.json();
const { name, email, password } = submittedData;
const { firstName, lastName, email, password } = submittedData;
const supabase = getServerSupabase();
const { data, error } = await supabase.auth.signUp({
email,
Expand Down Expand Up @@ -54,68 +56,72 @@ export async function POST(req: NextRequest) {
await prisma.user.create({
data: {
id: data.user.id,
slug: `${slugifyString(name)}-${generateHash(4, "0123456789")}`,
name,
slug: `${slugifyString(firstName)}${
lastName ? `-${slugifyString(lastName)}` : ""
}-${generateHash(4, "0123456789")}`,
firstName,
lastName: lastName || undefined,
email,
},
});

return NextResponse.json({}, { status: 201 });
})
});
}

export async function PUT(req: NextRequest) {
return await handleErrors(async () => {
const loginId = await getLoginId(req);
if (!loginId) {
throw new UnauthorizedError()
throw new UnauthorizedError();
}
const submittedData: UserPutBody = await req.json();
const { name } = submittedData;
const { firstName, lastName } = submittedData;
const currentData = await prisma.user.findUnique({
where: { id: loginId },
});
if (!currentData) {
throw new BadRequestError('Unable to find user')
throw new BadRequestError("Unable to find user");
}
const slugSuffix = getSlugSuffix(currentData.slug);
await prisma.user.update({
where: {
id: loginId,
},
data: {
slug: `${slugifyString(name)}-${slugSuffix}`,
name,
slug: `${slugifyString(firstName)}-${slugSuffix}`,
firstName,
lastName,
},
});
return NextResponse.json({}, { status: 200 });
})
});
}

// Used to determine if an email is available when a user attempts to change theirs
export async function GET(req: NextRequest) {
return await handleErrors(async () => {
const loginId = await getLoginId(req);
if (!loginId) {
throw new UnauthorizedError()
throw new UnauthorizedError();
}

const email = req.nextUrl.searchParams.get('email')
const email = req.nextUrl.searchParams.get("email");

if (!email) {
throw new BadRequestError()
throw new BadRequestError();
}

const emailUsed = await prisma.user.findUnique({
where: {
email
}
})
email,
},
});

if (emailUsed) {
throw new ForbiddenError('Email already in use')
throw new ForbiddenError("Email already in use");
}

return NextResponse.json({message: "Email is available"}, { status: 200 })
})
return NextResponse.json({ message: "Email is available" }, { status: 200 });
});
}
Loading