Skip to content

Commit

Permalink
feat: add account settings
Browse files Browse the repository at this point in the history
  • Loading branch information
brckd committed Oct 8, 2024
1 parent ea3c014 commit f9f0f3e
Show file tree
Hide file tree
Showing 10 changed files with 99 additions and 25 deletions.
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
npm config set legacy-peer-deps true
4 changes: 4 additions & 0 deletions src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +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,
};
14 changes: 14 additions & 0 deletions src/actions/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
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);
},
});
33 changes: 33 additions & 0 deletions src/actions/settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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))
},
});
14 changes: 1 addition & 13 deletions src/actions/signup.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { ActionError, defineAction } from "astro:actions";
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";
import { eq } from "drizzle-orm";

export const signup = defineAction({
accept: "form",
Expand All @@ -15,17 +14,6 @@ export const signup = defineAction({
password: z.string(),
}),
handler: async ({name, slug, password}, {cookies}) => {
const [existingUser] = await db
.select()
.from(users)
.where(eq(users.slug, slug));

if (existingUser)
throw new ActionError({
code: "BAD_REQUEST",
message: "The provided slug already exists.",
});

const id = generateIdFromEntropySize(10);
await db.insert(users).values({
id,
Expand Down
1 change: 1 addition & 0 deletions src/comps/forms/Button.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type Props = astroHTML.JSX.ButtonHTMLAttributes;
background: #eee;
border-radius: 0.5em;
text-align: center;
transition: background 0.2s;
}
button:hover {
cursor: pointer;
Expand Down
8 changes: 1 addition & 7 deletions src/comps/forms/Input.astro
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,7 @@ interface Props extends astroHTML.JSX.InputHTMLAttributes {
id?: string;
}
const {
name,
id = `${name}-input`,
label,
placeholder = "Click to type...",
type,
} = Astro.props;
const { name, id = `${name}-input`, label, placeholder, type } = Astro.props;
---

<div class="input-field">
Expand Down
2 changes: 2 additions & 0 deletions src/lib/schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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)),
Expand Down
7 changes: 2 additions & 5 deletions src/pages/account/index.astro
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
---
import Layout from "@layouts/Layout.astro";
const { user } = Astro.locals;
if (!user) return Astro.redirect("/account/signup");
if (user) return Astro.redirect("/account/settings");
else return Astro.redirect("/account/signup");
---

<Layout title="Account">Welcome, {user.name}!</Layout>
40 changes: 40 additions & 0 deletions src/pages/account/settings.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
import Layout from "@layouts/Layout.astro";
import Form from "@comps/forms/Form.astro";
import Input from "@comps/forms/Input.astro";
import Button from "@comps/forms/Button.astro";
import { actions } from "astro:actions";
const { user } = Astro.locals;
if (!user) return Astro.redirect("/account/signup");
---

<Layout title="Settings">
<Form method="POST" action={"/account/settings" + actions.settings}>
<Input name="name" label="Name" value={user.name} />
<Input name="slug" label="Handle" value={user.slug} />
<Input type="password" name="password" label="Password" />
<Button>Update</Button>
</Form>
<h2>Account Management</h2>
<Form>
<Button formmethod="POST" formaction={"/account/signup" + actions.logout}
>Log out</Button
>
</Form>
</Layout>

<script>
import { slug } from "@lib/schema";

const nameInput = document.getElementById("name-input") as HTMLInputElement;
const slugInput = document.getElementById("slug-input") as HTMLInputElement;

function setSlug(this: HTMLInputElement) {
const { success, data } = slug().safeParse(this.value);
if (success) slugInput.value = data;
}

nameInput.addEventListener("input", setSlug);
slugInput.addEventListener("input", setSlug);
</script>

0 comments on commit f9f0f3e

Please sign in to comment.