268 lines
7.8 KiB
TypeScript
268 lines
7.8 KiB
TypeScript
|
|
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;
|
||
|
|
};
|
||
|
|
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" ||
|
||
|
|
(loginType === "register" &&
|
||
|
|
(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: loginType === "register" && validateTeamId(teamId),
|
||
|
|
};
|
||
|
|
if (Object.values(fieldErrors).some(Boolean))
|
||
|
|
return badRequest({ fieldErrors, fields });
|
||
|
|
|
||
|
|
switch (loginType) {
|
||
|
|
case "login": {
|
||
|
|
const user = await login({ username, password });
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|