From 2eb10eb4d67637cc64f5cf7c77cd7b3709120692 Mon Sep 17 00:00:00 2001 From: Nathan Rashleigh Date: Tue, 29 Apr 2025 18:02:15 +1000 Subject: [PATCH] less and logins --- crates/CLAUDE.md | 1 + crates/deno.jsonc | 5 +- crates/fresh.config.ts | 5 +- crates/less.plugin.ts | 131 +++----- crates/main.ts | 5 + crates/package.json | 3 +- crates/routes/_404.tsx | 2 +- crates/routes/_app.tsx | 7 +- crates/routes/login.tsx | 28 +- crates/routes/register.tsx | 105 +++++-- crates/static/style/styles.less | 2 +- crates/static/swirl.fix.svg | 337 ++++++++++++++++++++ crates/static/swirl.svg | 541 ++++++++++++++++++++++++++++++++ 13 files changed, 1034 insertions(+), 138 deletions(-) create mode 100644 crates/CLAUDE.md create mode 100644 crates/static/swirl.fix.svg create mode 100644 crates/static/swirl.svg diff --git a/crates/CLAUDE.md b/crates/CLAUDE.md new file mode 100644 index 0000000..a47a032 --- /dev/null +++ b/crates/CLAUDE.md @@ -0,0 +1 @@ +- forms submitted should be validated using zod \ No newline at end of file diff --git a/crates/deno.jsonc b/crates/deno.jsonc index 0a9e5fb..4783064 100644 --- a/crates/deno.jsonc +++ b/crates/deno.jsonc @@ -21,6 +21,7 @@ "@twind/core": "https://esm.sh/@twind/core@1.1.3", "@twind/preset-tailwind": "https://esm.sh/@twind/preset-tailwind@1.1.4/", "@twind/preset-autoprefix": "https://esm.sh/@twind/preset-autoprefix@1.0.7/", + "less": "npm:less@4.2.0", "api/": "./api/", "api": "./api/", @@ -50,5 +51,7 @@ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" - } + }, + + "nodeModulesDir": "auto" } diff --git a/crates/fresh.config.ts b/crates/fresh.config.ts index 7116d99..56ee786 100644 --- a/crates/fresh.config.ts +++ b/crates/fresh.config.ts @@ -7,10 +7,7 @@ import lessPlugin from "./less.plugin.ts"; export default defineConfig({ plugins: [ // twind(twindConfig), - lessPlugin({ - path: "static/style", - outDir: "static/css" - }) + lessPlugin() ], middleware: { auth: authMiddleware diff --git a/crates/less.plugin.ts b/crates/less.plugin.ts index 9ceb536..0806f87 100644 --- a/crates/less.plugin.ts +++ b/crates/less.plugin.ts @@ -1,116 +1,59 @@ import { Plugin } from "$fresh/server.ts"; -import { ensureDir } from "$std/fs/ensure_dir.ts"; -import { join } from "$std/path/join.ts"; -import { parse } from "$std/path/parse.ts"; -import { dirname } from "$std/path/dirname.ts"; +import { ensureDir, exists } from "$std/fs/mod.ts"; +import { join } from "$std/path/mod.ts"; +import { dirname } from "$std/path/mod.ts"; -// External Less compiler for Deno -const LESS_CDN = "https://esm.sh/less@4.2.0"; +// Import Less directly +import less from "less"; -export interface LessPluginOptions { +interface LessPluginOptions { path: string; outDir: string; } -export default function lessPlugin(options: LessPluginOptions): Plugin { - let watcher: Deno.FsWatcher | null = null; - const combinedCssPath = join(options.outDir, "styles.combined.css"); - let less: any; +export default function lessPlugin(): Plugin { + const path = "static/style" + const outDir = "static/css" - // Compile a Less file and return its CSS - async function compileLessFile(path: string): Promise { + // Compile the Less file using the Less package + async function compileLess(inputPath: string, outputPath: string): Promise { try { - const lessContent = await Deno.readTextFile(path); - const { css } = await less.render(lessContent); - return css; - } catch (err) { - console.error(`Error compiling ${path}:`, err); - return ""; - } - } + const lessContent = await Deno.readTextFile(inputPath); + const lessOptions = { + filename: inputPath, // This helps with resolving imports + paths: [dirname(inputPath)], + }; - // Combine all compiled CSS into a single file - async function combineAndWriteCss(paths) { - try { - const compiledCssArray: string[] = []; - - for (const path of paths) { - const css = await compileLessFile(path); - compiledCssArray.push(css); - } - - const combinedCss = compiledCssArray.join("\n"); - await Deno.writeTextFile(combinedCssPath, combinedCss); - console.log(`Combined CSS written to ${combinedCssPath}`); - } catch (err) { - console.error("Error combining CSS:", err); - } - } - - // Start watching the style directory - async function startWatcher() { - try { - // Determine directory to watch (assuming first path is representative) - const watchDir = dirname(options.path); - - console.log(`Starting watcher on ${watchDir}`); - watcher = Deno.watchFs(watchDir); - - (async () => { - for await (const event of watcher!) { - if (event.kind === "modify" || event.kind === "create") { - const lessFiles = event.paths.filter(p => p.endsWith(".less")); - if (lessFiles.length > 0) { - console.log(`Less files changed: ${lessFiles.join(", ")}`); - await combineAndWriteCss(lessFiles); - } - } - } - })(); - } catch (err) { - console.error("Error starting watcher:", err); + const output = await less.render(lessContent, lessOptions); + await Deno.writeTextFile(outputPath, output.css); + console.log(`Compiled ${inputPath} to ${outputPath}`); + } catch (error) { + console.error("Less compilation error:", error); } } return { - name: "less", + name: "lessPlugin", async buildStart() { - try { - // Import the Less compiler - const lessModule = await import(LESS_CDN); - less = lessModule.default; + // Ensure output directory exists + const outputDir = join(Deno.cwd(), outDir); + await ensureDir(outputDir); - // Ensure the output directory exists - await ensureDir(options.outDir); + // Only process index.less which imports the others + const inputPath = join(Deno.cwd(), path, "index.less"); + const outputPath = join(outputDir, "style.css"); - // Initial compilation - await combineAndWriteCss(); - - // Start the file watcher - await startWatcher(); - } catch (err) { - console.error("Error initializing Less plugin:", err); - } + // Initial compilation + await compileLess(inputPath, outputPath); + console.log("Less styles compiled during build start"); }, - render(ctx) { - // Generate a link to the combined CSS file - const href = "/css/styles.combined.css" - ctx.render(); - return { - links: [{ rel: "stylesheet", href }], - }; - }, + // async renderAsync(ctx) { + // await ctx.renderAsync(); - async buildEnd() { - // Clean up watcher when build ends - if (watcher) { - try { - watcher.close(); - } catch (err) { - console.error("Error closing watcher:", err); - } - } - }, + // // Add stylesheet link to the result links + // const cssPath = `/css/style.css`; + // return { links: [{ rel: "stylesheet", href: cssPath }] } + // } }; } \ No newline at end of file diff --git a/crates/main.ts b/crates/main.ts index 675f529..6e5d242 100644 --- a/crates/main.ts +++ b/crates/main.ts @@ -10,4 +10,9 @@ import { start } from "$fresh/server.ts"; import manifest from "./fresh.gen.ts"; import config from "./fresh.config.ts"; +import lessPlugin from './less.plugin.ts' + +const less = lessPlugin() +await less.buildStart() + await start(manifest, config); diff --git a/crates/package.json b/crates/package.json index b723340..f490fc0 100644 --- a/crates/package.json +++ b/crates/package.json @@ -3,6 +3,7 @@ "prisma": "^6.6.0" }, "dependencies": { - "@prisma/client": "^6.6.0" + "@prisma/client": "^6.6.0", + "zod": "^3.24.3" } } diff --git a/crates/routes/_404.tsx b/crates/routes/_404.tsx index c63ae2e..3a564e1 100644 --- a/crates/routes/_404.tsx +++ b/crates/routes/_404.tsx @@ -10,7 +10,7 @@ export default function Error404() {
the Fresh logo: a sliced lemon dripping with juice + + crates - - +
+ +
); diff --git a/crates/routes/login.tsx b/crates/routes/login.tsx index 6c65bb8..d0f4e28 100644 --- a/crates/routes/login.tsx +++ b/crates/routes/login.tsx @@ -1,6 +1,7 @@ import { h } from "preact"; import { Handlers, PageProps } from "$fresh/server.ts"; import { getCookies, setCookie } from "$std/http/cookie.ts"; +import { z } from "npm:zod"; import Swirl from "../components/swirl.tsx"; import { sessions } from "../api/auth.middleware.ts"; import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; @@ -24,13 +25,32 @@ export const handler: Handlers = { async POST(req, ctx) { const formData = await req.formData(); - const email = formData.get("email")?.toString(); - const password = formData.get("password")?.toString(); - if (!email || !password) { - return ctx.render({ error: "Email and password required" }); + // Define login schema with Zod + const LoginSchema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters") + }); + + // Validate the form data + const result = LoginSchema.safeParse({ + email: formData.get("email"), + password: formData.get("password") + }); + + if (!result.success) { + // Format validation errors + const fieldErrors = result.error.flatten().fieldErrors; + const firstError = + fieldErrors.email?.[0] || + fieldErrors.password?.[0] || + "Invalid form data"; + + return ctx.render({ error: firstError }); } + const { email, password } = result.data; + try { // Create pg instance for database operations const pg = createPg(console); diff --git a/crates/routes/register.tsx b/crates/routes/register.tsx index 9620cca..4fe7eff 100644 --- a/crates/routes/register.tsx +++ b/crates/routes/register.tsx @@ -1,9 +1,10 @@ import { h } from "preact"; import { Handlers, PageProps } from "$fresh/server.ts"; -import { getCookies } from "$std/http/cookie.ts"; -import { useState } from "preact/hooks"; +import { getCookies, setCookie } from "$std/http/cookie.ts"; +import { z } from "npm:zod"; import Swirl from "../components/swirl.tsx"; import { sessions } from "../api/auth.middleware.ts"; +import { createPg } from "../api/pg.ts"; export const handler: Handlers = { async GET(req, ctx) { @@ -20,45 +21,89 @@ export const handler: Handlers = { return ctx.render(); }, -}; - -function Register() { - const [error, setError] = useState(""); - - async function handleSubmit(e) { - e.preventDefault(); - setError(""); + async POST(req, ctx) { + const form = await req.formData(); - const email = e.target.email.value; - const password = e.target.password.value; - const confirm = e.target.confirm.value; + // Define register schema with Zod + const RegisterSchema = z.object({ + email: z.string().email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), + confirm: z.string() + }).refine((data) => data.password === data.confirm, { + message: "Passwords do not match", + path: ["confirm"] + }); - if (password !== confirm) { - setError("passwords do not match"); - return; + // Validate the form data + const result = RegisterSchema.safeParse({ + email: form.get("email"), + password: form.get("password"), + confirm: form.get("confirm") + }); + + if (!result.success) { + // Format validation errors + const fieldErrors = result.error.flatten().fieldErrors; + const firstError = + fieldErrors.email?.[0] || + fieldErrors.password?.[0] || + fieldErrors.confirm?.[0] || + "Invalid form data"; + + return ctx.render({ error: firstError }); } + const { email, password } = result.data; + try { - const response = await fetch("/api/auth/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ email, password }), - }); + // Create pg instance for database operations + const pg = createPg(console); - const data = await response.json(); + // Check if user exists + const existingUser = await pg.userFindEmail(email); - if (!response.ok) { - throw new Error(data.error || "Registration failed"); + if (existingUser) { + return ctx.render({ error: "Email already registered" }); } - // Redirect on successful registration - window.location.href = "/"; + // Create user + const params = { email, password, data: {} }; + const user = await pg.userCreate(params); + + // Create session + const sessionId = crypto.randomUUID(); + const expiresAt = new Date(); + expiresAt.setDate(expiresAt.getDate() + 7); // Session expires in 7 days + + sessions.set(sessionId, { + userId: user.id, + expiresAt + }); + + // Set cookie and redirect + const headers = new Headers({ Location: "/" }); + setCookie(headers, { + name: "session", + value: sessionId, + expires: expiresAt, + path: "/", + httpOnly: true, + secure: true, + sameSite: "Lax" + }); + + return new Response(null, { + status: 303, + headers + }); } catch (err) { - setError(err.message); + console.error("Registration error:", err); + return ctx.render({ error: "An error occurred during registration" }); } } +}; + +function Register({ error = "" }: { error?: string }) { const errorEl = error ?
{error}
@@ -67,7 +112,7 @@ function Register() { return (