Skip to content

Commit

Permalink
feat: move account pages from club page
Browse files Browse the repository at this point in the history
  • Loading branch information
brckd committed Oct 9, 2024
1 parent a4f5432 commit 82687eb
Show file tree
Hide file tree
Showing 20 changed files with 420 additions and 0 deletions.
11 changes: 11 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { login } from "./login";
import { signup } from "./signup";
import { settings } from "./settings";
import { logout } from "./logout";

export const server = {
login,
signup,
settings,
logout,
};
36 changes: 36 additions & 0 deletions src/actions/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:schema";
import { slug } from "@lib/schema";
import { db, users } from "@lib/db";
import { lucia, hashOptions } from "@lib/auth/index";
import { verify } from "@node-rs/argon2";
import { eq } from "drizzle-orm";

export const login = defineAction({
accept: "form",
input: z.object({
slug: slug(),
password: z.string(),
}),
handler: async ({ slug, password }, { cookies }) => {
const authError = new ActionError({
code: "BAD_REQUEST",
message: "The entered slug or password is incorrect.",
});

const [user] = await db.select().from(users).where(eq(users.slug, slug));
if (!user) throw authError;

const passwordValid = await verify(user.password, password, hashOptions);
if (!passwordValid) throw authError;

const session = await lucia.createSession(user.id, {});
const sessionCookie = lucia.createSessionCookie(session.id);

cookies.set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);
},
});
15 changes: 15 additions & 0 deletions src/actions/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ActionError, defineAction } from "astro:actions";
import { lucia } from "@lib/auth/index";

export const logout = defineAction({
accept: "form",
handler: async (_, { locals: { user, session } }) => {
if (!user || !session)
throw new ActionError({
code: "UNAUTHORIZED",
message: "You must be logged in to log out.",
});

await lucia.invalidateSession(session.id);
},
});
36 changes: 36 additions & 0 deletions src/actions/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ActionError, defineAction } from "astro:actions";
import { z } from "astro:schema";
import { emptyString, slug } from "@lib/schema";
import { db, users } from "@lib/db";
import { hashOptions } from "@lib/auth/index";
import { hash } from "@node-rs/argon2";
import { eq } from "drizzle-orm";

export const settings = defineAction({
accept: "form",
input: z.object({
name: emptyString().nullable().or(z.string()),
slug: emptyString().nullable().or(slug()),
password: emptyString().nullable().or(z.string()),
}),
handler: async ({ name, slug, password }, { locals: { user } }) => {
if (!user)
throw new ActionError({
code: "UNAUTHORIZED",
message: "You need to be logged in to edit your preferences.",
});

if (name == user.name) name = null;
if (slug == user.slug) slug = null;
if (!(name || slug || password)) return;

await db
.update(users)
.set({
name: name ?? undefined,
slug: slug ?? undefined,
password: password ? await hash(password, hashOptions) : undefined,
})
.where(eq(users.id, user.id));
},
});
34 changes: 34 additions & 0 deletions src/actions/signup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { defineAction } from "astro:actions";
import { z } from "astro:schema";
import { slug } from "@lib/schema";
import { db, users } from "@lib/db";
import { lucia, hashOptions } from "@lib/auth/index";
import { hash } from "@node-rs/argon2";
import { generateIdFromEntropySize } from "lucia";

export const signup = defineAction({
accept: "form",
input: z.object({
name: z.string(),
slug: slug(),
password: z.string(),
}),
handler: async ({ name, slug, password }, { cookies }) => {
const id = generateIdFromEntropySize(10);
await db.insert(users).values({
id,
name,
slug,
password: await hash(password, hashOptions),
});

const session = await lucia.createSession(id, {});
const sessionCookie = lucia.createSessionCookie(session.id);

cookies.set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);
},
});
Binary file added src/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions src/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare namespace App {
interface Locals {
session: import("lucia").Session | null;
user: import("lucia").User | null;
}
}
57 changes: 57 additions & 0 deletions src/layouts/Layout.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
import Icon from "@assets/icon.png";
import "@fontsource-variable/baloo-2";
import Header from "@fancade-club/comps/layouts/Header.astro";
import Footer from "@fancade-club/comps/layouts/Footer.astro";
import Disclaimer from "@fancade-club/comps/layouts/Disclaimer.astro";
import NavLink from "@fancade-club/comps/layouts/NavLink.astro"
interface Props {
title?: string;
description?: string;
}
const { title = "", description = "" } = Astro.props;
const metaDescription =
description ||
"The Fancade Club is a worldwide community of Fancade enthusiasts. Join and become a member today!";
const metaTitle = (title && title + " - ") + "Fancade Club Account";
---

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content={metaDescription} />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href={Icon.src} />
<meta name="generator" content={Astro.generator} />
<title>{metaTitle}</title>
</head>
<body>
<Header title="Account" image={Icon}><NavLink href="https://fancade.club">Club</NavLink></Header>
<main>
{title && <h1>{title}</h1>}
<slot />
</main>
<Footer><Disclaimer/> </Footer>
</body>

<style>
body {
min-height: 100vh;
color: #333333;
font-size: 14pt;
font-family: "Baloo 2 Variable", system-ui;
font-weight: 500;
text-align: justify;
}
main {
min-height: 90vh;
max-width: 800px;
margin: 0 auto;
}
</style>
</html>
34 changes: 34 additions & 0 deletions src/lib/auth/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Lucia } from "lucia";
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { db, sessions, users } from "@lib/db";

export const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);

export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: import.meta.env.PROD,
},
},
getUserAttributes: ({ name, slug }) => ({
name,
slug,
}),
});

export const hashOptions = {
memoryCost: 19456,
timeCost: 2,
hashLength: 32,
parallelism: 1,
};

declare module "lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: {
name: string;
slug: string;
};
}
}
2 changes: 2 additions & 0 deletions src/lib/db/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./schema";
export * from "./init";
7 changes: 7 additions & 0 deletions src/lib/db/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import { DATABASE_URL } from "astro:env/server";

export const sql = neon(DATABASE_URL);
export const db = drizzle(sql);
export type DB = typeof db;
19 changes: 19 additions & 0 deletions src/lib/db/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
id: text("id").primaryKey(),
slug: text("slug").unique().notNull(),
password: text("password").notNull(),
name: text("name").notNull(),
});

export const sessions = pgTable("sessions", {
id: text("id").primaryKey(),
userId: text("user_id")
.notNull()
.references(() => users.id),
expiresAt: timestamp("expires_at", {
withTimezone: true,
mode: "date",
}).notNull(),
});
13 changes: 13 additions & 0 deletions src/lib/schema/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "astro:schema";
import slugify from "@sindresorhus/slugify";
import zxcvbn from "zxcvbn";

export const emptyString = () => z.literal("").transform(() => null);

export const slug = () =>
z.preprocess(
(val) => slugify(z.string().parse(val)),
z.string().min(3).max(16),
);

export const password = () => z.string().refine((val) => zxcvbn(val).score > 4);
32 changes: 32 additions & 0 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { lucia } from "@lib/auth";
import { defineMiddleware } from "astro:middleware";

export const onRequest = defineMiddleware(async (context, next) => {
const sessionId = context.cookies.get(lucia.sessionCookieName)?.value ?? null;
if (!sessionId) {
context.locals.user = null;
context.locals.session = null;
return next();
}

const { session, user } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
context.cookies.set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);
}
if (!session) {
const sessionCookie = lucia.createBlankSessionCookie();
context.cookies.set(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);
}
context.locals.session = session;
context.locals.user = user;
return next();
});
10 changes: 10 additions & 0 deletions src/pages/404.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
import Layout from "@layouts/Layout.astro";
---

<Layout title="Not Found">
<p>
The page you tried to access doesn't exist. In case you tried to access a
distinct service, it's likely it doesn't exist yet, or hasn't been approved.
</p>
</Layout>
11 changes: 11 additions & 0 deletions src/pages/500.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
import Layout from "@layouts/Layout.astro";
---

<Layout title="Server Error">
<p>
An internal server error occured while rendering this page. Please <a
href="/contact">contribute</a
> by reporting this bug and describe the sequence of actions that lead you here.
</p>
</Layout>
5 changes: 5 additions & 0 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
const { user } = Astro.locals;
if (user) return Astro.redirect("/settings");
else return Astro.redirect("/signup");
---
18 changes: 18 additions & 0 deletions src/pages/login.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
import { actions } from "astro:actions";
import Layout from "@layouts/Layout.astro";
import Form from "@fancade-club/comps/forms/Form.astro";
import Input from "@fancade-club/comps/forms/Input.astro";
import Button from "@fancade-club/comps/forms/Button.astro";
if (Astro.locals.user) return Astro.redirect("/account");
---

<Layout title="Log In">
<Form method="POST" action={"/" + actions.login}>
<Input required name="slug" label="Handle" />
<Input required type="password" name="password" label="Password" />
<Button>Log in</Button>
</Form>
<p>Not a member yet? <a href="/signup">Sign up</a>!</p>
</Layout>
Loading

0 comments on commit 82687eb

Please sign in to comment.