explit/app/routes/login.tsx

268 lines
7.8 KiB
TypeScript
Raw Normal View History

import type { ActionFunction, LinksFunction, MetaFunction } from "remix";
import { useActionData, json, Link, useSearchParams, Form } from "remix";
import { db } from "~/utils/db.server";
import { login, createUserSession, register } from "~/utils/session.server";
export const links: LinksFunction = () => {
return [];
};
export const meta: MetaFunction = () => {
return {
title: "Explit | Login",
description: "Login to track and split your expenses!",
};
};
function validateUsername(username: unknown) {
if (typeof username !== "string" || username.length < 3) {
return `Usernames must be at least 3 characters long`;
}
}
function validatePassword(password: unknown) {
if (typeof password !== "string" || password.length < 6) {
return `Passwords must be at least 6 characters long`;
}
}
function validateTeamId(teamId: unknown) {
if (typeof teamId !== "string" || teamId.length < 1) {
return "You must indicate an arbitrary team ID";
}
}
type ActionData = {
formError?: string;
fieldErrors?: {
username: string | undefined;
password: string | undefined;
teamId: string | undefined;
};
fields?: {
loginType: string;
username: string;
password: string;
teamId: string;
icon?: string;
};
};
const badRequest = (data: ActionData) => json(data, { status: 400 });
export const action: ActionFunction = async ({ request }) => {
const form = await request.formData();
const loginType = form.get("loginType");
const username = form.get("username");
const password = form.get("password");
const icon = form.get("icon");
const teamId = form.get("teamId");
const redirectTo = form.get("redirectTo") || "/expenses";
if (
typeof loginType !== "string" ||
typeof username !== "string" ||
typeof password !== "string" ||
typeof icon !== "string" ||
typeof teamId !== "string" ||
typeof redirectTo !== "string"
) {
return badRequest({
formError: `Form not submitted correctly.`,
});
}
const fields = { loginType, username, password, teamId };
const fieldErrors = {
username: validateUsername(username),
password: validatePassword(password),
teamId: validateTeamId(teamId),
};
if (Object.values(fieldErrors).some(Boolean))
return badRequest({ fieldErrors, fields });
switch (loginType) {
case "login": {
const user = await login({ username, password, icon, teamId });
if (!user) {
return badRequest({
fields,
formError: `Username/Password combination is incorrect`,
});
}
return createUserSession(user.id, redirectTo);
}
case "register": {
const userExists = await db.user.findFirst({
where: { username },
});
if (userExists) {
console.error(userExists);
return badRequest({
fields,
formError: `User with username ${username} already exists`,
});
}
const user = await register({ username, password, icon, teamId });
if (!user) {
return badRequest({
fields,
formError: `Something went wrong trying to create a new user.`,
});
}
return createUserSession(user.id, redirectTo);
}
default: {
return badRequest({
fields,
formError: `Login type invalid`,
});
}
}
};
export default function Login() {
const actionData = useActionData<ActionData>();
const [searchParams] = useSearchParams();
return (
<div className="container">
<div className="content" data-light="">
<h1>Login</h1>
<Form
method="post"
aria-describedby={
actionData?.formError ? "form-error-message" : undefined
}
>
<input
type="hidden"
name="redirectTo"
value={searchParams.get("redirectTo") ?? undefined}
/>
<fieldset>
<legend className="sr-only">Login or Register?</legend>
<label>
<input
type="radio"
name="loginType"
value="login"
defaultChecked={
!actionData?.fields?.loginType ||
actionData?.fields?.loginType === "login"
}
/>{" "}
Login
</label>
<label>
<input
type="radio"
name="loginType"
value="register"
defaultChecked={actionData?.fields?.loginType === "register"}
/>{" "}
Register
</label>
</fieldset>
<div>
<label htmlFor="username-input">Username</label>
<input
type="text"
id="username-input"
name="username"
defaultValue={actionData?.fields?.username}
aria-invalid={Boolean(actionData?.fieldErrors?.username)}
aria-describedby={
actionData?.fieldErrors?.username ? "username-error" : undefined
}
/>
{actionData?.fieldErrors?.username ? (
<p
className="form-validation-error"
role="alert"
id="username-error"
>
{actionData?.fieldErrors.username}
</p>
) : null}
</div>
<div>
<label htmlFor="username-input">Team ID</label>
<input
type="text"
id="team-id-input"
name="teamId"
defaultValue={actionData?.fields?.teamId}
aria-hidden={actionData?.fields?.loginType === "login"}
aria-invalid={Boolean(actionData?.fieldErrors?.teamId)}
aria-describedby={
actionData?.fieldErrors?.teamId ? "teamId-error" : undefined
}
/>
{actionData?.fieldErrors?.teamId ? (
<p
className="form-validation-error"
role="alert"
id="teamId-error"
>
{actionData?.fieldErrors.teamId}
</p>
) : null}
</div>
<div>
<label htmlFor="username-input">Icon (letter or emoji)</label>
<input
type="text"
maxLength={1}
aria-hidden={actionData?.fields?.loginType === "login"}
id="icon-input"
name="icon"
defaultValue={actionData?.fields?.icon}
/>
</div>
<div>
<label htmlFor="password-input">Password</label>
<input
id="password-input"
name="password"
defaultValue={actionData?.fields?.password}
type="password"
aria-invalid={
Boolean(actionData?.fieldErrors?.password) || undefined
}
aria-describedby={
actionData?.fieldErrors?.password ? "password-error" : undefined
}
/>
{actionData?.fieldErrors?.password ? (
<p
className="form-validation-error"
role="alert"
id="password-error"
>
{actionData?.fieldErrors.password}
</p>
) : null}
</div>
<div id="form-error-message">
{actionData?.formError ? (
<p className="form-validation-error" role="alert">
{actionData?.formError}
</p>
) : null}
</div>
<button type="submit" className="button">
Submit
</button>
</Form>
</div>
<div className="links">
<ul>
<li>
<Link to="/">Home</Link>
</li>
</ul>
</div>
</div>
);
}