145 lines
3.9 KiB
TypeScript
145 lines
3.9 KiB
TypeScript
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";
|
|
import { createPg } from "../api/pg.ts";
|
|
|
|
export const handler: Handlers = {
|
|
async GET(req, ctx) {
|
|
const cookies = getCookies(req.headers);
|
|
const sessionId = cookies.session;
|
|
|
|
// If user is already logged in, redirect to home
|
|
if (sessionId && sessions.has(sessionId)) {
|
|
return new Response("", {
|
|
status: 307,
|
|
headers: { Location: "/" },
|
|
});
|
|
}
|
|
|
|
return ctx.render();
|
|
},
|
|
|
|
async POST(req, ctx) {
|
|
const formData = await req.formData();
|
|
|
|
// 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);
|
|
|
|
// Find user by email
|
|
const user = await pg.userFindEmail(email);
|
|
|
|
if (!user) {
|
|
return ctx.render({ error: "Invalid credentials" });
|
|
}
|
|
|
|
// Verify password
|
|
const match = await bcrypt.compare(password, user.password);
|
|
|
|
if (!match) {
|
|
return ctx.render({ error: "Invalid credentials" });
|
|
}
|
|
|
|
// 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 (error) {
|
|
console.error("Login error:", error);
|
|
return ctx.render({ error: "An error occurred during login" });
|
|
}
|
|
}
|
|
};
|
|
|
|
function Login({ error }: { error?: string }) {
|
|
const errorEl = error
|
|
? <div class="alt-text">{error}</div>
|
|
: null;
|
|
|
|
return (
|
|
<div class="login">
|
|
{errorEl}
|
|
<form method="POST" action="/login">
|
|
<div class="item">
|
|
<label for="email">email</label>
|
|
<input type="email" name="email" id="email" required />
|
|
</div>
|
|
<div class="item">
|
|
<label for="password">password</label>
|
|
<input type="password" name="password" id="password" required />
|
|
</div>
|
|
<div class="item">
|
|
<input type="submit" value="login" />
|
|
</div>
|
|
</form>
|
|
<a href="/register" class="button">create account</a>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function LoginPage(props: PageProps) {
|
|
const { error } = props.data || {};
|
|
|
|
return (
|
|
<div class="landing">
|
|
<Login error={error} />
|
|
<Swirl />
|
|
<div class="words">
|
|
<p>record importing service</p>
|
|
<p>we bundle orders together from thousands of online stores to reduce the cost of shipping vinyl</p>
|
|
<p>then we get together and play them</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |