Skip to content
This repository was archived by the owner on Aug 25, 2024. It is now read-only.
Draft
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
13 changes: 13 additions & 0 deletions app/components/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Link } from "@remix-run/react";

export function ProfileMenu() {
// For now we just always display a login link: this is terrible UX, but simple.
// TODO show here if the user is logged in or not.
return <GoToLogin />
}

function GoToLogin() {
return (
<Link to="/login">Log in</Link>
);
}
3 changes: 3 additions & 0 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from "@remix-run/react";
import { SpeedInsights } from "@vercel/speed-insights/remix"

import { ProfileMenu } from "~/components/profile";

import faviconSvg from "~/images/favicon.svg";

import logo from "~/images/icon.svg";
Expand Down Expand Up @@ -53,6 +55,7 @@ export default function App() {
<li><NavLink to="/my-schedule/" prefetch="intent">Your Schedule</NavLink></li>
<li><NavLink to="/events/" prefetch="intent">All Events</NavLink></li>
<li><NavLink to="/settings/">Settings</NavLink></li>
<li><ProfileMenu /></li>
</ul>
</nav>
</header>
Expand Down
80 changes: 80 additions & 0 deletions app/routes/login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { Form, useActionData } from "@remix-run/react";
import { authenticator } from "~/scripts/auth.server";
import { json } from "@remix-run/node";

import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";

export const meta: MetaFunction = () => {
return [
{ title: "Log in | BVS" },
{ name: "description", content: "Log in to your account for personalized content." },
];
};

export async function loader({ request }: LoaderFunctionArgs) {
// If the user is already authenticated redirect to home
return await authenticator.isAuthenticated(request, {
successRedirect: getNextUrl(request),
});
}

function getNextUrl(request: Request) {
try {
const url = new URL(request.url).searchParams.get("next");
if (typeof url !== "string") {
return "/";
}
try {
return new URL(url).pathname; // No cross-origin redirects happening here!
} catch {
return url;
}
} catch (e) {
return "/";
}
}

export async function action({ request }: ActionFunctionArgs) {
try {
return await authenticator.authenticate("usernamePassword", request, {
successRedirect: getNextUrl(request),
throwOnError: true
});
} catch (e) {
switch ((e as Error).message) {
case "badUsername":
return json({ formError: { message: "User not found" } });
break;
default:
return json({
formError: {
message: `Unknown authentication error "${(e as Error).message}"`
}
});
}
}
}

export default function Login() {
const actionData = useActionData<typeof action>();
return (
<Form method="post" replace encType="multipart/form-data">
<input
type="text"
name="username"
placeholder="Username"
autoComplete="username"
required
/><br />
<input
type="password"
name="password"
placeholder="Password "
autoComplete="current-password"
required
/><br />
<button>Sign In</button>
<div>{actionData?.formError && <b>{actionData.formError.message}</b>}</div>
</Form>
);
}
11 changes: 10 additions & 1 deletion app/routes/my-schedule.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { MetaFunction } from "@remix-run/node";
import { authenticator } from "~/scripts/auth.server";

import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";

export const meta: MetaFunction = () => {
return [
Expand All @@ -7,6 +9,13 @@ export const meta: MetaFunction = () => {
];
};

export async function loader({ request }: LoaderFunctionArgs) {
// If the user is already authenticated redirect to home
return await authenticator.isAuthenticated(request, {
failureRedirect: "/login?next=/my-schedule",
});
}

export default function MySchedule() {
return (
<h1>My Schedule</h1>
Expand Down
9 changes: 9 additions & 0 deletions app/scripts/accounts.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { prisma } from "~/scripts/prisma.server";

export async function getUserByUsername(username: string) {
return await prisma.user.findUnique({ where: { username } });
}

export async function usernameExists(username: string) {
return await getUserByUsername(username) !== null;
}
39 changes: 39 additions & 0 deletions app/scripts/auth.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Authenticator } from "remix-auth";
import { FormStrategy } from "remix-auth-form";
import { sessionStorage } from "~/scripts/session.server";
import { usernamePasswordLogin } from "~/scripts/password.server";
import { prisma } from "~/scripts/prisma.server";

import type { Session } from "~/scripts/session.server";
import type { CredType } from "@prisma/client";

export const authenticator = new Authenticator<Session>(sessionStorage);

// Tell the Authenticator to use the form strategy
authenticator.use(
new FormStrategy(async ({ form }) => {
let username = form.get("username");
let password = form.get("password");
if (typeof username !== "string" || typeof password !== "string") {
throw new TypeError("badFormData");
}
const session = await usernamePasswordLogin(
username,
password,
await getCredsByType(username, "USERNAME_PASSWORD")
);
return session;
}),
"usernamePassword"
);

async function getCredsByType(username: string, type: CredType) {
return await prisma.cred.findMany({
where: {
user: {
username
},
type
}
});
}
31 changes: 31 additions & 0 deletions app/scripts/password.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import bcrypt from "bcryptjs";
import { getUserByUsername } from "~/scripts/accounts.server";

import type { Cred } from "@prisma/client";
import type { Session } from "~/scripts/session.server";

export async function usernamePasswordLogin(
username: string,
password: string,
passwordCreds: Cred[]
) {
const user = await getUserByUsername(username);
if (user == null) {
throw new Error("badUsername");
}
if (passwordCreds.length == 0) {
throw new Error("noPasswords");
}
if (password.length == 0) {
throw new Error("missingPassword");
}

for (const cred of passwordCreds) {
if (await bcrypt.compare(password, (cred.cred as { hash: string }).hash)) {
let session: Session = { user };
return session;
}
}

throw new Error("badPassword");
}
60 changes: 60 additions & 0 deletions app/scripts/session.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { prisma } from "~/scripts/prisma.server";
import { createSessionStorage } from "@remix-run/node";
import type { User } from "@prisma/client";

export type Session = {
user: User
};

function databaseSessionStorage(cookie: any) {
return createSessionStorage({
cookie,
async createData(data) {
const id = `${Date.now()}_${Math.floor(Math.random() * 2 ** 31)}`;
await prisma.session.create({
data: {
id,
data
}
});
return id;
},
async readData(id) {
return await prisma.session.findUnique({
where: { id }
})
},
async updateData(id, data) {
await prisma.session.update({
where: { id },
data: {
data
}
});
},
async deleteData(id) {
await prisma.session.delete({
where: { id }
});
},
});
}

const sessionSecret = process.env.NODE_ENV === "production" ? process.env.SESSION_SECRET : "dev";
if (!sessionSecret) {
throw new Error("SESSION_SECRET must be set in prod");
}

export const sessionStorage = databaseSessionStorage({
cookie: {
name: "BVS_session",
secure: process.env.NODE_ENV === "production",
secrets: [sessionSecret],
sameSite: "lax",
path: "/",
maxAge: 60 * 60 * 24 * 30,
httpOnly: true,
},
});

export const { getSession, commitSession, destroySession } = sessionStorage;
3 changes: 2 additions & 1 deletion app/styles/global-wide.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ header.bvs-header > nav a {
padding: 0.8rem var(--edge-padding);
text-decoration: none;
font-weight: bold;
border-bottom: 0.2rem solid transparent;
}

header.bvs-header > nav a.active {
border-bottom: 0.2rem solid;
border-bottom-color: white;
}

footer.bvs-footer {
Expand Down
44 changes: 44 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading