diff --git a/.vscode/settings.json b/.vscode/settings.json
index c567e8e..211503f 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -16,5 +16,16 @@
},
"files.associations": {
"*.css": "tailwindcss"
+ },
+ "files.exclude": {
+ "**/.git": true,
+ "**/.svn": true,
+ "**/.hg": true,
+ "**/.DS_Store": true,
+ "**/Thumbs.db": true,
+ "**/.cursor": true,
+ "**/.vscode": true,
+ "**/_fresh": true,
+ "**/node_modules": true
}
}
diff --git a/components/Header.tsx b/components/Header.tsx
index d2915d2..6bcef14 100644
--- a/components/Header.tsx
+++ b/components/Header.tsx
@@ -7,7 +7,6 @@ const loggedOutLinks = [
const loggedInLinks = [
{ href: "/", label: "Home" },
{ href: "/dashboard", label: "Dashboard" },
- { href: "/sign-out", label: "Sign out" },
];
export interface Props {
@@ -29,6 +28,13 @@ export default function Header(props: Props) {
{l.label}
))}
+ {props.loggedIn && (
+
+
+
+ )}
diff --git a/islands/PasswordConfirmationFields.tsx b/islands/PasswordConfirmationFields.tsx
new file mode 100644
index 0000000..2667b8e
--- /dev/null
+++ b/islands/PasswordConfirmationFields.tsx
@@ -0,0 +1,67 @@
+import { useRef, useState } from "preact/hooks";
+
+const mismatchMessage = "Passwords do not match.";
+
+export default function PasswordConfirmationFields() {
+ const [password, setPassword] = useState("");
+ const [passwordConfirm, setPasswordConfirm] = useState("");
+ const confirmInput = useRef(null);
+ const passwordsDoNotMatch = passwordConfirm.length > 0 &&
+ password !== passwordConfirm;
+
+ function updatePassword(value: string) {
+ setPassword(value);
+ const confirmation = confirmInput.current;
+ if (confirmation) {
+ confirmation.setCustomValidity(
+ confirmation.value && value !== confirmation.value
+ ? mismatchMessage
+ : "",
+ );
+ }
+ }
+
+ function updatePasswordConfirm(value: string) {
+ setPasswordConfirm(value);
+ confirmInput.current?.setCustomValidity(
+ value && password !== value ? mismatchMessage : "",
+ );
+ }
+
+ return (
+
+
updatePassword(event.currentTarget.value)}
+ />
+
+
updatePasswordConfirm(event.currentTarget.value)}
+ />
+ {passwordsDoNotMatch && (
+
+ {mismatchMessage}
+
+ )}
+
+
+ );
+}
diff --git a/lib/auth.ts b/lib/auth.ts
new file mode 100644
index 0000000..392db5c
--- /dev/null
+++ b/lib/auth.ts
@@ -0,0 +1,27 @@
+import { deleteCookie, getCookies, setCookie } from "@std/http/cookie";
+
+export function setAuthCookie(
+ headers: Headers,
+) {
+ setCookie(headers, {
+ name: "auth",
+ value: "ok",
+ maxAge: 60 * 60,
+ sameSite: "Lax",
+ path: "/",
+ secure: true,
+ httpOnly: true,
+ });
+}
+
+export function unsetAuthCookie(headers: Headers) {
+ deleteCookie(headers, "auth", {
+ path: "/",
+ });
+}
+
+export function isLoggedIn(req: Request): boolean {
+ const cookies = getCookies(req.headers);
+ const authCookie = cookies["auth"];
+ return authCookie === "ok";
+}
diff --git a/routes/(app)/_middleware.ts b/routes/(app)/_middleware.ts
new file mode 100644
index 0000000..5e9ed32
--- /dev/null
+++ b/routes/(app)/_middleware.ts
@@ -0,0 +1,9 @@
+import { define } from "../../utils.ts";
+
+export default define.middleware((ctx) => {
+ if (!ctx.state.loggedIn) {
+ return ctx.redirect("/sign-in", 303);
+ }
+
+ return ctx.next();
+});
diff --git a/routes/(app)/dashboard.tsx b/routes/(app)/dashboard.tsx
new file mode 100644
index 0000000..0b3d679
--- /dev/null
+++ b/routes/(app)/dashboard.tsx
@@ -0,0 +1,10 @@
+import Headline from "../../components/Headline.tsx";
+import { define } from "../../utils.ts";
+
+export default define.page(function Dashboard() {
+ return (
+
+ Dashboard
+
+ );
+});
diff --git a/routes/sign-out.tsx b/routes/(app)/sign-out.tsx
similarity index 52%
rename from routes/sign-out.tsx
rename to routes/(app)/sign-out.tsx
index c6b7dfd..65794ef 100644
--- a/routes/sign-out.tsx
+++ b/routes/(app)/sign-out.tsx
@@ -1,13 +1,12 @@
-import { define, unsetAuthCookie } from "../utils.ts";
+import { unsetAuthCookie } from "../../lib/auth.ts";
+import { define } from "../../utils.ts";
export const handler = define.handlers({
- GET(ctx) {
+ POST(ctx) {
const headers = new Headers();
unsetAuthCookie(headers);
- const url = new URL(ctx.req.url);
- url.pathname = "/";
- headers.set("location", url.toString());
+ headers.set("location", new URL("/", ctx.req.url).toString());
return new Response(null, {
status: 303,
headers,
diff --git a/routes/(guest)/_middleware.ts b/routes/(guest)/_middleware.ts
new file mode 100644
index 0000000..fa0eca4
--- /dev/null
+++ b/routes/(guest)/_middleware.ts
@@ -0,0 +1,9 @@
+import { define } from "../../utils.ts";
+
+export default define.middleware((ctx) => {
+ if (ctx.state.loggedIn) {
+ return ctx.redirect("/dashboard", 303);
+ }
+
+ return ctx.next();
+});
diff --git a/routes/sign-in.tsx b/routes/(guest)/sign-in.tsx
similarity index 76%
rename from routes/sign-in.tsx
rename to routes/(guest)/sign-in.tsx
index 4812b50..e115b1a 100644
--- a/routes/sign-in.tsx
+++ b/routes/(guest)/sign-in.tsx
@@ -1,8 +1,9 @@
import { z } from "zod";
-import { backendSignIn } from "../backend/sign-in.ts";
-import Headline from "../components/Headline.tsx";
-import { SignInForm } from "../components/SignInForm.tsx";
-import { define, isLoggedIn, setAuthCookie } from "../utils.ts";
+import { backendSignIn } from "../../backend/sign-in.ts";
+import Headline from "../../components/Headline.tsx";
+import { SignInForm } from "../../components/SignInForm.tsx";
+import { setAuthCookie } from "../../lib/auth.ts";
+import { define } from "../../utils.ts";
const loginSchema = z.object({
email: z.string(),
@@ -10,13 +11,7 @@ const loginSchema = z.object({
});
export const handler = define.handlers({
- GET(ctx) {
- if (isLoggedIn(ctx.req)) {
- const url = new URL(ctx.req.url);
- url.pathname = "/dashboard";
- return Response.redirect(url, 303);
- }
-
+ GET(_ctx) {
return {
data: {
email: "",
@@ -52,7 +47,7 @@ export const handler = define.handlers({
}
const headers = new Headers();
- setAuthCookie(ctx.req, headers);
+ setAuthCookie(headers);
const url = new URL(ctx.req.url);
url.pathname = "/dashboard";
diff --git a/routes/(guest)/sign-up.tsx b/routes/(guest)/sign-up.tsx
new file mode 100644
index 0000000..61475f7
--- /dev/null
+++ b/routes/(guest)/sign-up.tsx
@@ -0,0 +1,97 @@
+import { z } from "zod";
+import Headline from "../../components/Headline.tsx";
+import PasswordConfirmationFields from "../../islands/PasswordConfirmationFields.tsx";
+import { define } from "../../utils.ts";
+
+const signUpSchema = z.object({
+ email: z.email("Please enter a valid email address.").trim(),
+ firstName: z.string().trim().min(1, "Please enter your first name."),
+ lastName: z.string().trim().min(1, "Please enter your last name."),
+ password: z.string().min(8, "Password must be at least 8 characters long."),
+ passwordConfirm: z.string(),
+}).refine((data) => data.password === data.passwordConfirm, {
+ message: "Passwords do not match.",
+ path: ["passwordConfirm"],
+});
+
+const emptyForm = {
+ email: "",
+ firstName: "",
+ lastName: "",
+ error: undefined as string | undefined,
+};
+
+export const handler = define.handlers({
+ GET() {
+ return { data: emptyForm };
+ },
+
+ async POST(ctx) {
+ const formData = await ctx.req.formData();
+ const result = signUpSchema.safeParse({
+ email: formData.get("email"),
+ firstName: formData.get("firstName"),
+ lastName: formData.get("lastName"),
+ password: formData.get("password"),
+ passwordConfirm: formData.get("passwordConfirm"),
+ });
+
+ if (!result.success) {
+ return {
+ data: {
+ email: formData.get("email")?.toString() ?? "",
+ firstName: formData.get("firstName")?.toString() ?? "",
+ lastName: formData.get("lastName")?.toString() ?? "",
+ error: result.error.issues[0].message,
+ },
+ };
+ }
+
+ const { passwordConfirm: _passwordConfirm, ...registration } = result.data;
+ // Use registration to register user
+ void registration;
+
+ return ctx.redirect("/sign-up", 303);
+ },
+});
+
+export default define.page(function SignUpPage(ctx) {
+ const { email, firstName, lastName, error } = ctx.data;
+
+ return (
+
+ );
+});
diff --git a/routes/index.tsx b/routes/(public)/index.tsx
similarity index 90%
rename from routes/index.tsx
rename to routes/(public)/index.tsx
index ef1cc7d..805cc46 100644
--- a/routes/index.tsx
+++ b/routes/(public)/index.tsx
@@ -1,7 +1,7 @@
import { useSignal } from "@preact/signals";
import { Head } from "fresh/runtime";
-import Counter from "../islands/Counter.tsx";
-import { define } from "../utils.ts";
+import Counter from "../../islands/Counter.tsx";
+import { define } from "../../utils.ts";
export default define.page(function Home(ctx) {
const count = useSignal(3);
diff --git a/routes/_app.tsx b/routes/_app.tsx
index 120814d..9227f16 100644
--- a/routes/_app.tsx
+++ b/routes/_app.tsx
@@ -1,9 +1,8 @@
import Header from "../components/Header.tsx";
-import { define, isLoggedIn } from "../utils.ts";
+import { define } from "../utils.ts";
export default define.page(function App(ctx) {
const Component = ctx.Component;
- const loggedIn = isLoggedIn(ctx.req);
return (
@@ -13,7 +12,7 @@ export default define.page(function App(ctx) {
Deno Fresh Dummy
-
+