diff --git a/app/models/profile.server.ts b/app/models/profile.server.ts index f61ea6bc..dc4f6ac2 100644 --- a/app/models/profile.server.ts +++ b/app/models/profile.server.ts @@ -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({ @@ -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>, + userId: User["id"], + username: Profile["username"], +) { + return transaction + .insert(profile) + .values({ + username, + public: false, + userId, + }); +} \ No newline at end of file diff --git a/app/models/user.server.ts b/app/models/user.server.ts index ad0c75cb..7ab536fc 100644 --- a/app/models/user.server.ts +++ b/app/models/user.server.ts @@ -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, @@ -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 }, @@ -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( diff --git a/app/routes/explore.register.tsx b/app/routes/explore.register.tsx index d067950a..d082d753 100644 --- a/app/routes/explore.register.tsx +++ b/app/routes/explore.register.tsx @@ -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"; @@ -46,7 +46,7 @@ export async function action({ request }: ActionFunctionArgs) { return data( { errors: { - username: "UserName is required", + username: "username_required", email: null, password: null, }, @@ -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 }, ); } @@ -82,7 +89,7 @@ export async function action({ request }: ActionFunctionArgs) { { errors: { username: null, - password: "Password is required", + password: "password_required", email: null, }, }, @@ -95,7 +102,7 @@ export async function action({ request }: ActionFunctionArgs) { { errors: { username: null, - password: "Password is too short", + password: "password_too_short", email: null, }, }, @@ -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, }, }, @@ -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, @@ -179,17 +182,17 @@ export default function RegisterDialog() { )}
- Register + {t('register')} - Create a new account to get started. + {t('create_account')}
- + {actionData?.errors?.username && (
- {actionData.errors.username} + {t(actionData.errors.username)}
)}
- + {actionData?.errors?.email && (
- {actionData.errors.email} + {t(actionData.errors.email)}
)}
- + {actionData?.errors?.password && (
- {actionData.errors.password} + {t(actionData.errors.password)}
)}
- +
- {t("already_account_label")}{" "} + {t("already_account")}{" "} - {t("login_label")} + {t("login")}
diff --git a/app/routes/join.tsx b/app/routes/join.tsx index 3b803ba1..0fb16912 100644 --- a/app/routes/join.tsx +++ b/app/routes/join.tsx @@ -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, @@ -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 }, ); } @@ -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 }, ); } @@ -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 }, @@ -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, }, }, @@ -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, }, @@ -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(); @@ -172,7 +174,7 @@ export default function Join() { /> {actionData?.errors?.name && (
- {actionData.errors.name} + {t(actionData.errors.name)}
)} @@ -200,7 +202,7 @@ export default function Join() { /> {actionData?.errors?.email && (
- {actionData.errors.email} + {t(actionData.errors.email)}
)} @@ -226,7 +228,7 @@ export default function Join() { /> {actionData?.errors?.password && (
- {actionData.errors.password} + {t(actionData.errors.password)}
)} diff --git a/app/utils.ts b/app/utils.ts index 345b1341..6bdac5d2 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -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 }; } diff --git a/public/locales/de/register.json b/public/locales/de/register.json index 5f087ae9..417dbd55 100644 --- a/public/locales/de/register.json +++ b/public/locales/de/register.json @@ -1,9 +1,22 @@ { - "register_label": "Registrieren", - "email_label": "E-Mail", - "password_label": "Passwort", - "account_label": "Konto erstellen", - "transition_label": "Erstelle Konto...", - "already_account_label": "Bereits ein Konto?", - "login_label": "Einloggen" + "register": "Registrieren", + "create_account": "Um loszulegen, erstelle einen neuen Account.", + "username": "Benutzername", + "enter_username": "Gib deinen Benutzernamen ein", + "email": "E-Mail", + "enter_email": "Gib deine E-Mail Adresse ein", + "password": "Passwort", + "enter_password": "Gib dein Passwort ein", + "account": "Konto erstellen", + "transition": "Erstelle Konto...", + "already_account": "Bereits ein Konto?", + "login": "Einloggen", + "username_required": "Benutzername ist ein Pflichtfeld", + "username_already_taken": "Der Benutzername ist bereits vergeben", + "username_min_characters": "Bitte nutze mindestens 4 Zeichen", + "username_invalid": "Der Benutzername beinhaltet ungültige Zeichen", + "password_required": "Passwort ist ein Pflichtfeld", + "password_too_short": "Passwort ist zu kurz", + "email_already_taken": "Die E-Mail Adresse wird bereits verwendet", + "email_invalid": "Ungültige E-Mail Adresse" } \ No newline at end of file diff --git a/public/locales/en/register.json b/public/locales/en/register.json index 3864f0cb..ca57cb0e 100644 --- a/public/locales/en/register.json +++ b/public/locales/en/register.json @@ -1,9 +1,22 @@ { - "register_label": "Sign up", - "email_label": "Email", - "password_label": "Password", - "account_label": "Create account", - "transition_label": "Creating account...", - "already_account_label": "Already have an account?", - "login_label": "Log in" + "register": "Sign up", + "create_account": "Create a new account to get started.", + "username": "Username", + "enter_username": "Enter your username", + "email": "Email", + "enter_email": "Enter your email address", + "password": "Password", + "enter_password": "Enter your password", + "account": "Create account", + "transition": "Creating account...", + "already_account": "Already have an account?", + "login": "Log in", + "username_required": "Username is required", + "username_already_taken": "This username is already taken", + "username_min_characters": "Please use at least 4 characters.", + "username_invalid": "Username has invalid characters", + "password_required": "Password is required", + "password_too_short": "Password is too short", + "email_already_taken": "A user with this email already exists", + "email_invalid": "Invalid email address" } \ No newline at end of file