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
28 changes: 21 additions & 7 deletions app/models/profile.server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { eq } from "drizzle-orm";
import { eq, type ExtractTablesWithRelations } from "drizzle-orm";
import { type PgTransaction } from "drizzle-orm/pg-core";
import { type PostgresJsQueryResultHKT } from "drizzle-orm/postgres-js";
import { drizzleClient } from "~/db.server";
import { type User, type Profile, profile } from "~/schema";
import { type User, type Profile, profile } from "~/schema";
import type * as schema from "~/schema";

export async function getProfileByUserId(id: Profile["id"]) {
return drizzleClient.query.profile.findFirst({
Expand Down Expand Up @@ -45,9 +48,20 @@ export async function createProfile(
userId: User["id"],
username: Profile["username"],
) {
return drizzleClient.insert(profile).values({
username,
public: false,
userId,
});
return drizzleClient.transaction(t =>
createProfileWithTransaction(t, userId, username));
}

export async function createProfileWithTransaction(
transaction: PgTransaction<PostgresJsQueryResultHKT, typeof schema, ExtractTablesWithRelations<typeof schema>>,
userId: User["id"],
username: Profile["username"],
) {
return transaction
.insert(profile)
.values({
username,
public: false,
userId,
});
}
43 changes: 23 additions & 20 deletions app/models/user.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import crypto from "node:crypto";
import bcrypt from "bcryptjs";
import { eq } from "drizzle-orm";
import { v4 as uuidv4 } from "uuid";
import { createProfile } from "./profile.server";
import { createProfileWithTransaction } from "./profile.server";
import { drizzleClient } from "~/db.server";
import {
type Password,
Expand All @@ -23,6 +23,12 @@ export async function getUserByEmail(email: User["email"]) {
});
}

export async function getUserByUsername(username: User["name"]) {
return drizzleClient.query.user.findFirst({
where: (user, { eq }) => eq(user.name, username),
});
}

// export async function getUserWithDevicesByName(name: User["name"]) {
// return prisma.user.findUnique({
// where: { name },
Expand Down Expand Up @@ -128,26 +134,23 @@ export async function createUser(
) {
const hashedPassword = await bcrypt.hash(preparePasswordHash(password), 13); // make salt_factor configurable oSeM API uses 13 by default

// Maybe wrap in a transaction
// https://stackoverflow.com/questions/76082778/drizzle-orm-how-do-you-insert-in-a-parent-and-child-table
const newUser = await drizzleClient
.insert(user)
.values({
name,
email,
language,
unconfirmedEmail: email,
})
.returning();

await drizzleClient.insert(passwordTable).values({
hash: hashedPassword,
userId: newUser[0].id,
return await drizzleClient.transaction(async (t) => {
const newUser = await t
.insert(user)
.values({
name,
email,
language,
unconfirmedEmail: email,
})
.returning();
await t.insert(passwordTable).values({
hash: hashedPassword,
userId: newUser[0].id,
});
await createProfileWithTransaction(t, newUser[0].id, name);
return newUser;
});

await createProfile(newUser[0].id, name);

return newUser;
}

export async function verifyLogin(
Expand Down
51 changes: 27 additions & 24 deletions app/routes/explore.register.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
CardHeader,
CardTitle,
} from "~/components/ui/card";
import { createUser, getUserByEmail } from "~/models/user.server";
import { createUser, getUserByEmail, getUserByUsername } from "~/models/user.server";
import { safeRedirect, validateEmail, validateName } from "~/utils";
import { createUserSession, getUserId } from "~/utils/session.server";

Expand All @@ -46,7 +46,7 @@ export async function action({ request }: ActionFunctionArgs) {
return data(
{
errors: {
username: "UserName is required",
username: "username_required",
email: null,
password: null,
},
Expand All @@ -70,9 +70,16 @@ export async function action({ request }: ActionFunctionArgs) {
);
}

const existingUsername = await getUserByUsername(username);
if(existingUsername)
return data(
{ errors: { username: "username_already_taken", email: null, password: null } },
{ status: 400 },
);

if (!validateEmail(email)) {
return data(
{ errors: { username: null, email: "Email is invalid", password: null } },
{ errors: { username: null, email: "email_invalid", password: null } },
{ status: 400 },
);
}
Expand All @@ -82,7 +89,7 @@ export async function action({ request }: ActionFunctionArgs) {
{
errors: {
username: null,
password: "Password is required",
password: "password_required",
email: null,
},
},
Expand All @@ -95,7 +102,7 @@ export async function action({ request }: ActionFunctionArgs) {
{
errors: {
username: null,
password: "Password is too short",
password: "password_too_short",
email: null,
},
},
Expand All @@ -110,7 +117,7 @@ export async function action({ request }: ActionFunctionArgs) {
{
errors: {
username: null,
email: "A user already exists with this email",
email: "email_already_taken",
password: null,
},
},
Expand All @@ -124,11 +131,7 @@ export async function action({ request }: ActionFunctionArgs) {
const locale = await i18next.getLocale(request);
const language = locale === "de" ? "de_DE" : "en_US";

//* temp -> dummy name
// const name = "Max Mustermann";

const user = await createUser(username, email, language, password);
// const user = await createUser(email, password, username?.toString());

return createUserSession({
request,
Expand Down Expand Up @@ -179,34 +182,34 @@ export default function RegisterDialog() {
)}
<Form method="post" className="space-y-6" noValidate>
<CardHeader>
<CardTitle className="text-2xl font-bold">Register</CardTitle>
<CardTitle className="text-2xl font-bold">{t('register')}</CardTitle>
<CardDescription>
Create a new account to get started.
{t('create_account')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="username">Username</Label>
<Label htmlFor="username">{t('username')}</Label>
<Input
id="username"
placeholder="Enter your username"
placeholder={t('enter_username')}
ref={usernameRef}
name="username"
type="text"
autoFocus={true}
/>
{actionData?.errors?.username && (
<div className="text-sm text-red-500 mt-1" id="password-error">
{actionData.errors.username}
{t(actionData.errors.username)}
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="email">{t("email_label")}</Label>
<Label htmlFor="email">{t("email")}</Label>
<Input
id="email"
type="email"
placeholder="Enter your email"
placeholder={t('enter_email')}
ref={emailRef}
required
autoFocus={true}
Expand All @@ -217,16 +220,16 @@ export default function RegisterDialog() {
/>
{actionData?.errors?.email && (
<div className="text-sm text-red-500 mt-1" id="email-error">
{actionData.errors.email}
{t(actionData.errors.email)}
</div>
)}
</div>
<div className="space-y-2">
<Label htmlFor="password">{t("password_label")}</Label>
<Label htmlFor="password">{t("password")}</Label>
<Input
id="password"
type="password"
placeholder="Enter your password"
placeholder={t('enter_password')}
ref={passwordRef}
name="password"
autoComplete="new-password"
Expand All @@ -235,17 +238,17 @@ export default function RegisterDialog() {
/>
{actionData?.errors?.password && (
<div className="text-sm text-red-500 mt-1" id="password-error">
{actionData.errors.password}
{t(actionData.errors.password)}
</div>
)}
</div>
</CardContent>
<CardFooter className="flex flex-col items-center gap-2">
<Button className="w-full bg-light-blue">Register</Button>
<Button className="w-full bg-light-blue">{t('register')}</Button>
<div className="text-sm text-muted-foreground">
{t("already_account_label")}{" "}
{t("already_account")}{" "}
<Link to="/explore/login" className="underline">
{t("login_label")}
{t("login")}
</Link>
</div>
</CardFooter>
Expand Down
20 changes: 11 additions & 9 deletions app/routes/join.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import i18next from "app/i18next.server";
import * as React from "react";
import { useTranslation } from "react-i18next";
import {
type ActionFunctionArgs,
type LoaderFunctionArgs,
Expand Down Expand Up @@ -34,7 +35,7 @@ export async function action({ request }: ActionFunctionArgs) {

if (!name || typeof name !== "string") {
return data(
{ errors: { name: "Name is required", email: null, password: null } },
{ errors: { name: "username_required", email: null, password: null } },
{ status: 400 },
);
}
Expand All @@ -56,14 +57,14 @@ export async function action({ request }: ActionFunctionArgs) {

if (!validateEmail(email)) {
return data(
{ errors: { name: null, email: "Email is invalid", password: null } },
{ errors: { name: null, email: "email_invalid", password: null } },
{ status: 400 },
);
}

if (typeof password !== "string" || password.length === 0) {
return data(
{ errors: { name: null, email: null, password: "Password is required" } },
{ errors: { name: null, email: null, password: "password_required" } },
{ status: 400 },
);
}
Expand All @@ -74,7 +75,7 @@ export async function action({ request }: ActionFunctionArgs) {
errors: {
name: null,
email: null,
password: "Please use at least 8 characters.",
password: "password_too_short",
},
},
{ status: 400 },
Expand All @@ -88,7 +89,7 @@ export async function action({ request }: ActionFunctionArgs) {
{
errors: {
name: null,
email: "A user already exists with this email",
email: "email_already_taken",
password: null,
},
},
Expand All @@ -102,7 +103,7 @@ export async function action({ request }: ActionFunctionArgs) {
return data(
{
errors: {
name: "A user already exists with this name",
name: "username_already_taken",
email: null,
password: null,
},
Expand Down Expand Up @@ -130,6 +131,7 @@ export const meta: MetaFunction = () => {
};

export default function Join() {
const { t } = useTranslation("register");
const [searchParams] = useSearchParams();
const redirectTo = searchParams.get("redirectTo") ?? undefined;
const actionData = useActionData<typeof action>();
Expand Down Expand Up @@ -172,7 +174,7 @@ export default function Join() {
/>
{actionData?.errors?.name && (
<div className="pt-1 text-[#FF0000]" id="email-error">
{actionData.errors.name}
{t(actionData.errors.name)}
</div>
)}
</div>
Expand Down Expand Up @@ -200,7 +202,7 @@ export default function Join() {
/>
{actionData?.errors?.email && (
<div className="pt-1 text-[#FF0000]" id="email-error">
{actionData.errors.email}
{t(actionData.errors.email)}
</div>
)}
</div>
Expand All @@ -226,7 +228,7 @@ export default function Join() {
/>
{actionData?.errors?.password && (
<div className="pt-1 text-[#FF0000]" id="password-error">
{actionData.errors.password}
{t(actionData.errors.password)}
</div>
)}
</div>
Expand Down
16 changes: 10 additions & 6 deletions app/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,16 @@ export function validateEmail(email: unknown): email is string {
* @deprecated Use {@link validateUsername} instead
*/
export function validateName(name: string) {
const { required, length, invalidCharacters } = validateUsername(name);
if (required) return { isValid: false, errorMsg: "Name is required" };
else if (length)
return { isValid: false, errorMsg: "Please use at least 4 characters." };
else if (invalidCharacters)
return { isValid: false, errorMsg: "Name is invalid" };
if (name.length === 0) {
return { isValid: false, errorMsg: "username_required" };
} else if (name.length < 4) {
return { isValid: false, errorMsg: "username_min_characters" };
} else if (
name &&
!/^[a-zA-Z0-9][a-zA-Z0-9\s._-]+[a-zA-Z0-9-_.]$/.test(name.toString())
) {
return { isValid: false, errorMsg: "username_invalid" };
}

return { isValid: true };
}
Expand Down
Loading
Loading