mw stuff
This commit is contained in:
1 parent
bdc22c5c42
commit
a4d8608a3e
16 files changed
+261
-304
No files matched your search
@@ -1 +1,3 @@
|
||||
- forms submitted should be validated using zod
|
||||
- packages should always be added to deno.jsonc as import maps
|
||||
- errors should be handled in the mw.error.ts and thrown as custom errors
|
||||
@@ -0,0 +1,80 @@
|
||||
// Base custom error class
|
||||
export class AppError extends Error {
|
||||
status: number
|
||||
details?: Record<string, unknown>
|
||||
override name: string
|
||||
|
||||
constructor(message: string, status = 500, details?: Record<string, unknown>) {
|
||||
super(message)
|
||||
this.name = this.constructor.name
|
||||
this.status = status
|
||||
this.details = details
|
||||
}
|
||||
}
|
||||
|
||||
// Validation error
|
||||
export class ValidationError extends AppError {
|
||||
constructor(message = "Validation failed", details?: Record<string, unknown>) {
|
||||
super(message, 400, details)
|
||||
this.name = "ValidationError"
|
||||
}
|
||||
}
|
||||
|
||||
// Authentication errors
|
||||
export class AuthError extends AppError {
|
||||
constructor(message = "Authentication failed", status = 401) {
|
||||
super(message, status)
|
||||
this.name = "AuthError"
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidCredentialsError extends AuthError {
|
||||
constructor(message = "Invalid credentials") {
|
||||
super(message)
|
||||
this.name = "InvalidCredentialsError"
|
||||
}
|
||||
}
|
||||
|
||||
export class NotLoggedInError extends AuthError {
|
||||
constructor(message = "Not logged in") {
|
||||
super(message)
|
||||
this.name = "NotLoggedInError"
|
||||
}
|
||||
}
|
||||
|
||||
export class SessionExpiredError extends AuthError {
|
||||
constructor(message = "Session expired") {
|
||||
super(message)
|
||||
this.name = "SessionExpiredError"
|
||||
}
|
||||
}
|
||||
|
||||
// Resource errors
|
||||
export class ResourceError extends AppError {
|
||||
constructor(message: string, status = 404) {
|
||||
super(message, status)
|
||||
this.name = "ResourceError"
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceNotFoundError extends ResourceError {
|
||||
constructor(resource = "Resource") {
|
||||
super(`${resource} not found`)
|
||||
this.name = "ResourceNotFoundError"
|
||||
}
|
||||
}
|
||||
|
||||
export class ResourceExistsError extends ResourceError {
|
||||
constructor(resource = "Resource") {
|
||||
super(`${resource} already exists`, 409)
|
||||
this.name = "ResourceExistsError"
|
||||
}
|
||||
}
|
||||
|
||||
// Server error
|
||||
export class ServerError extends AppError {
|
||||
constructor(message = "An unexpected error occurred") {
|
||||
super(message, 500)
|
||||
this.name = "ServerError"
|
||||
}
|
||||
}
|
||||
+57
-11
@@ -7,9 +7,49 @@ const PROTECTED = [
|
||||
"/home",
|
||||
]
|
||||
|
||||
// Sessions storage shared between routes
|
||||
// In production, this would be Redis or a database
|
||||
export const sessions = new Map<string, { userId: string, expiresAt: Date }>()
|
||||
// Open Deno KV database
|
||||
const kv = await Deno.openKv();
|
||||
|
||||
// Session prefix for KV
|
||||
const SESSION_PREFIX = "session:";
|
||||
|
||||
// Helper functions for session management
|
||||
export async function createSession(userId: string, expirationHours = 24): Promise<{sessionId: string, expiresAt: Date}> {
|
||||
const sessionId = crypto.randomUUID();
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setHours(expiresAt.getHours() + expirationHours);
|
||||
|
||||
await kv.set([SESSION_PREFIX, sessionId], {
|
||||
userId,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
});
|
||||
|
||||
return { sessionId, expiresAt };
|
||||
}
|
||||
|
||||
export async function deleteSession(sessionId: string): Promise<void> {
|
||||
await kv.delete([SESSION_PREFIX, sessionId]);
|
||||
}
|
||||
|
||||
export async function getSession(sessionId: string): Promise<{ userId: string, expiresAt: Date } | null> {
|
||||
if (!sessionId) return null;
|
||||
|
||||
const sessionRes = await kv.get([SESSION_PREFIX, sessionId]);
|
||||
const session = sessionRes.value as { userId: string, expiresAt: string } | null;
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
// Check if session is expired
|
||||
if (new Date() > new Date(session.expiresAt)) {
|
||||
await deleteSession(sessionId);
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
userId: session.userId,
|
||||
expiresAt: new Date(session.expiresAt)
|
||||
};
|
||||
}
|
||||
|
||||
export async function authMiddleware(req: Request, ctx: CratesCtx): Promise<Response> {
|
||||
const { pg } = ctx.state
|
||||
@@ -23,8 +63,6 @@ export async function authMiddleware(req: Request, ctx: CratesCtx): Promise<Resp
|
||||
|
||||
// Allow public paths without authentication
|
||||
if (!PROTECTED.includes(path)) {
|
||||
|
||||
console.log('skip', path)
|
||||
return await ctx.next()
|
||||
}
|
||||
|
||||
@@ -43,14 +81,22 @@ export async function authMiddleware(req: Request, ctx: CratesCtx): Promise<Resp
|
||||
})
|
||||
}
|
||||
|
||||
// Verify session
|
||||
const session = sessions.get(sessionId)
|
||||
// Verify session using getSession helper
|
||||
const session = await getSession(sessionId);
|
||||
|
||||
if (!session || new Date() > session.expiresAt) {
|
||||
if (session) {
|
||||
sessions.delete(sessionId)
|
||||
if (!session) {
|
||||
if (path.startsWith("/api/")) {
|
||||
return new Response(JSON.stringify({ error: "Invalid or expired session" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
});
|
||||
}
|
||||
throw new Error("Invalid or expired session")
|
||||
|
||||
// Redirect to login page for non-API routes
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { Location: "/login" }
|
||||
});
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FreshContext } from "$fresh/server.ts"
|
||||
import { AppError } from "./errors.ts"
|
||||
|
||||
export async function errorMiddleware(req: Request, ctx: FreshContext): Promise<Response> {
|
||||
try {
|
||||
@@ -6,5 +7,28 @@ export async function errorMiddleware(req: Request, ctx: FreshContext): Promise<
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error(`Error handling ${req.method} ${new URL(req.url).pathname}:`, error)
|
||||
|
||||
// Check if it's a custom app error
|
||||
if (error instanceof AppError) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
error: error.message,
|
||||
...(error.details && { details: error.details })
|
||||
}),
|
||||
{
|
||||
status: error.status,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Handle unexpected errors
|
||||
return new Response(
|
||||
JSON.stringify({ error: "An unexpected error occurred" }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
function Ok(headers) {
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers
|
||||
})
|
||||
}
|
||||
+7
-2
@@ -1,11 +1,14 @@
|
||||
{
|
||||
"lock": false,
|
||||
|
||||
"unstable": ["kv"],
|
||||
|
||||
"tasks": {
|
||||
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
|
||||
"manifest": "deno task cli manifest $(pwd)",
|
||||
"start": "deno run -A --watch=routes/,.env dev.ts",
|
||||
"build": "deno run -A dev.ts build",
|
||||
"start": "deno run -A --unstable-kv --watch=routes/,.env dev.ts",
|
||||
"build": "deno run -A --unstable-kv dev.ts build",
|
||||
"preview": "deno run -A main.ts",
|
||||
"prisma-clean": "rm -rf prisma/generated",
|
||||
"prisma-generate": "deno task prisma-clean && deno run -A --env-file npm:prisma generate",
|
||||
@@ -28,6 +31,8 @@
|
||||
"less": "npm:less@4.2.0",
|
||||
"lucide-preact": "npm:lucide-preact@^0.475.0",
|
||||
"@prisma/client": "npm:@prisma/client@^6.6.0",
|
||||
"zodix": "npm:zodix@^0.4.4",
|
||||
|
||||
|
||||
"api/": "./api/",
|
||||
"api": "./api/",
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as $_404 from './routes/_404.tsx'
|
||||
import * as $_500 from './routes/_500.tsx'
|
||||
import * as $_app from './routes/_app.tsx'
|
||||
import * as $_middleware from './routes/_middleware.ts'
|
||||
import * as $api_middleware from './routes/api/_middleware.ts'
|
||||
import * as $api_auth_login from './routes/api/auth/login.ts'
|
||||
import * as $api_auth_logout from './routes/api/auth/logout.ts'
|
||||
import * as $api_auth_register from './routes/api/auth/register.ts'
|
||||
@@ -25,7 +24,6 @@ const manifest = {
|
||||
'./routes/_500.tsx': $_500,
|
||||
'./routes/_app.tsx': $_app,
|
||||
'./routes/_middleware.ts': $_middleware,
|
||||
'./routes/api/_middleware.ts': $api_middleware,
|
||||
'./routes/api/auth/login.ts': $api_auth_login,
|
||||
'./routes/api/auth/logout.ts': $api_auth_logout,
|
||||
'./routes/api/auth/register.ts': $api_auth_register,
|
||||
|
||||
+2
-1
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.6.0",
|
||||
"lodash": "^4.17.21",
|
||||
"zod": "^3.24.3"
|
||||
"zod": "^3.24.3",
|
||||
"zodix": "^0.4.4"
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,4 @@ import { errorMiddleware } from 'api/mw.error.ts'
|
||||
import { serviceMiddleware } from 'api/mw.service.ts'
|
||||
import { authMiddleware } from 'api/mw.auth.ts'
|
||||
|
||||
export const handler = [serviceMiddleware, authMiddleware]
|
||||
export const handler = [errorMiddleware, serviceMiddleware, authMiddleware]
|
||||
@@ -1,2 +0,0 @@
|
||||
import { authMiddleware } from 'api/mw.auth.ts'
|
||||
export const handler = [authMiddleware]
|
||||
@@ -2,54 +2,41 @@ import { Handlers } from "$fresh/server.ts"
|
||||
import { setCookie } from "$std/http/cookie.ts"
|
||||
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"
|
||||
|
||||
import { createPg } from "api/pg.ts"
|
||||
import { sessions } from "api/mw.auth.ts"
|
||||
import { z } from "npm:zod"
|
||||
import { zx } from "npm:zodix"
|
||||
|
||||
export const handler: Handlers = {
|
||||
async POST(req, _ctx) {
|
||||
import { createSession } from "api/mw.auth.ts"
|
||||
import {
|
||||
ValidationError,
|
||||
InvalidCredentialsError,
|
||||
NotLoggedInError,
|
||||
ServerError
|
||||
} from "api/errors.ts"
|
||||
import { CratesState } from 'api/mw.service.ts'
|
||||
|
||||
const LoginSchema = z.object({
|
||||
email: z.string().email("Invalid email format"),
|
||||
password: z.string().min(1)
|
||||
})
|
||||
|
||||
export const handler: Handlers<{email: string}, CratesState> = {
|
||||
async POST(req, ctx) {
|
||||
try {
|
||||
const data = await req.json()
|
||||
const { email, password } = data
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response(JSON.stringify({ error: "Email and password required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
}
|
||||
const { email, password } = await zx.parseForm(req, LoginSchema)
|
||||
|
||||
// Create pg instance for database operations
|
||||
const pg = createPg(console)
|
||||
const { pg } = ctx.state
|
||||
|
||||
// Find user by email
|
||||
const user = await pg.userFindEmail(email)
|
||||
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
}
|
||||
if (!user) throw new InvalidCredentialsError()
|
||||
|
||||
// Verify password
|
||||
const match = await bcrypt.compare(password, user.password)
|
||||
if (!match) throw new InvalidCredentialsError()
|
||||
|
||||
if (!match) {
|
||||
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
})
|
||||
// Create session using Deno KV
|
||||
const { sessionId, expiresAt } = await createSession(user.id, 24 * 7) // 7 days in hours
|
||||
|
||||
// Set cookie
|
||||
const headers = new Headers({ "Content-Type": "application/json" })
|
||||
@@ -71,11 +58,14 @@ export const handler: Handlers = {
|
||||
headers
|
||||
})
|
||||
} catch (error) {
|
||||
// Let the error middleware handle custom errors
|
||||
if (error instanceof ValidationError ||
|
||||
error instanceof InvalidCredentialsError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
console.error("Login error:", error)
|
||||
return new Response(JSON.stringify({ error: "An error occurred" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
throw new ServerError("An error occurred during login")
|
||||
}
|
||||
},
|
||||
|
||||
@@ -86,10 +76,7 @@ export const handler: Handlers = {
|
||||
const user = ctx.state.user
|
||||
|
||||
if (!user) {
|
||||
return new Response(JSON.stringify({ error: "Not logged in" }), {
|
||||
status: 401,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
throw new NotLoggedInError()
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Handlers } from "$fresh/server.ts"
|
||||
import { getCookies, setCookie } from "$std/http/cookie.ts"
|
||||
import { sessions } from "../../../api/mw.auth.ts"
|
||||
import { deleteSession } from "api/mw.auth.ts"
|
||||
|
||||
export const handler: Handlers = {
|
||||
async POST(req, _ctx) {
|
||||
@@ -8,8 +8,8 @@ export const handler: Handlers = {
|
||||
const sessionId = cookies.session
|
||||
|
||||
if (sessionId) {
|
||||
// Remove session from storage
|
||||
sessions.delete(sessionId)
|
||||
// Remove session from Deno KV
|
||||
await deleteSession(sessionId)
|
||||
}
|
||||
|
||||
// Clear the cookie
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { Handlers } from "$fresh/server.ts"
|
||||
import { setCookie } from "$std/http/cookie.ts"
|
||||
import { createPg } from "../../../api/pg.ts"
|
||||
import { sessions } from "../../../api/mw.auth.ts"
|
||||
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"
|
||||
import { createPg } from "api/pg.ts"
|
||||
import { createSession } from "api/mw.auth.ts"
|
||||
import {
|
||||
ValidationError,
|
||||
ResourceExistsError,
|
||||
ServerError
|
||||
} from "api/errors.ts"
|
||||
|
||||
// Define validation schema with Zod
|
||||
const RegisterSchema = z.object({
|
||||
email: z.string().email("Invalid email format"),
|
||||
password: z.string().min(6, "Password must be at least 6 characters"),
|
||||
discogs: z.string().optional()
|
||||
})
|
||||
|
||||
export const handler: Handlers = {
|
||||
async POST(req, _ctx) {
|
||||
try {
|
||||
const data = await req.json()
|
||||
const { discogs, email, password } = data
|
||||
const data = await req.formData()
|
||||
|
||||
if (!email || !password) {
|
||||
return new Response(JSON.stringify({ error: "Email and password required" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
// Validate input data with Zod
|
||||
const result = RegisterSchema.safeParse(data)
|
||||
|
||||
if (!result.success) {
|
||||
// Throw validation error
|
||||
throw new ValidationError("Validation failed", result.error.format())
|
||||
}
|
||||
|
||||
const { discogs, email, password } = result.data
|
||||
|
||||
// Create pg instance for database operations
|
||||
const pg = createPg(console)
|
||||
|
||||
@@ -23,25 +38,15 @@ export const handler: Handlers = {
|
||||
const existingUser = await pg.userFindEmail(email)
|
||||
|
||||
if (existingUser) {
|
||||
return new Response(JSON.stringify({ error: "Email already registered" }), {
|
||||
status: 400,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
throw new ResourceExistsError("User with this email")
|
||||
}
|
||||
|
||||
// Create user
|
||||
const params = { discogs, 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
|
||||
})
|
||||
// Create session using Deno KV
|
||||
const { sessionId, expiresAt } = await createSession(user.id, 24 * 7) // 7 days in hours
|
||||
|
||||
// Set cookie
|
||||
const headers = new Headers({ "Content-Type": "application/json" })
|
||||
@@ -64,11 +69,14 @@ export const handler: Handlers = {
|
||||
headers
|
||||
})
|
||||
} catch (error) {
|
||||
// Let the error middleware handle custom errors
|
||||
if (error instanceof ValidationError ||
|
||||
error instanceof ResourceExistsError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
console.error("Registration error:", error)
|
||||
return new Response(JSON.stringify({ error: "An error occurred" }), {
|
||||
status: 500,
|
||||
headers: { "Content-Type": "application/json" }
|
||||
})
|
||||
throw new ServerError("An error occurred during registration")
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-5
@@ -1,11 +1,16 @@
|
||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { createPg, Pg } from "../api/pg.ts";
|
||||
import Main from "../components/main.tsx";
|
||||
|
||||
import { CratesCtx } from "api/mw.service.ts"
|
||||
import { CratesState } from "api/mw.service.ts"
|
||||
import { Offer, Want } from "prismaClient"
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_req, ctx: CratesCtx) {
|
||||
interface HomeData {
|
||||
wants: Want[],
|
||||
offers: Offer[],
|
||||
}
|
||||
|
||||
export const handler: Handlers<HomeData, CratesState> = {
|
||||
async GET(_req, ctx) {
|
||||
const { pg, offers, user } = ctx.state
|
||||
|
||||
console.log(user)
|
||||
@@ -13,7 +18,7 @@ export const handler: Handlers = {
|
||||
const wants = await pg.userWants(user);
|
||||
const uOffers = await offers.userOffers(user);
|
||||
|
||||
return ctx.render({ wants, offers: uOffers });
|
||||
return await ctx.render({ wants, offers: uOffers });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
+2
-102
@@ -1,105 +1,5 @@
|
||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { getCookies, setCookie } from "$std/http/cookie.ts";
|
||||
import { z } from "npm:zod";
|
||||
import { PageProps } from "$fresh/server.ts";
|
||||
import Swirl from "../components/swirl.tsx";
|
||||
import { sessions } from "api/mw.auth.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: "/home" },
|
||||
});
|
||||
}
|
||||
|
||||
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: "/home" });
|
||||
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
|
||||
@@ -109,7 +9,7 @@ function Login({ error }: { error?: string }) {
|
||||
return (
|
||||
<div class="login">
|
||||
{errorEl}
|
||||
<form method="POST" action="/login">
|
||||
<form method="POST" action="/api/auth/login">
|
||||
<div class="item">
|
||||
<label for="email">email</label>
|
||||
<input type="email" name="email" id="email" required />
|
||||
|
||||
+1
-104
@@ -1,107 +1,4 @@
|
||||
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/mw.auth.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 form = await req.formData();
|
||||
|
||||
// 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"]
|
||||
});
|
||||
|
||||
// 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 {
|
||||
// Create pg instance for database operations
|
||||
const pg = createPg(console);
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = await pg.userFindEmail(email);
|
||||
|
||||
if (existingUser) {
|
||||
return ctx.render({ error: "Email already registered" });
|
||||
}
|
||||
|
||||
// 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) {
|
||||
console.error("Registration error:", err);
|
||||
return ctx.render({ error: "An error occurred during registration" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function Register({ error = "" }: { error?: string }) {
|
||||
|
||||
@@ -112,7 +9,7 @@ function Register({ error = "" }: { error?: string }) {
|
||||
return (
|
||||
<div class="login">
|
||||
{errorEl}
|
||||
<form action="/register" method="post">
|
||||
<form action="/api/auth/register" method="post">
|
||||
<div class="item">
|
||||
<label for="email">email</label>
|
||||
<input type="email" name="email" id="email" required />
|
||||
|
||||
Reference in new issue
Block a user