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
|
- 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",
|
"/home",
|
||||||
]
|
]
|
||||||
|
|
||||||
// Sessions storage shared between routes
|
// Open Deno KV database
|
||||||
// In production, this would be Redis or a database
|
const kv = await Deno.openKv();
|
||||||
export const sessions = new Map<string, { userId: string, expiresAt: Date }>()
|
|
||||||
|
// 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> {
|
export async function authMiddleware(req: Request, ctx: CratesCtx): Promise<Response> {
|
||||||
const { pg } = ctx.state
|
const { pg } = ctx.state
|
||||||
@@ -23,8 +63,6 @@ export async function authMiddleware(req: Request, ctx: CratesCtx): Promise<Resp
|
|||||||
|
|
||||||
// Allow public paths without authentication
|
// Allow public paths without authentication
|
||||||
if (!PROTECTED.includes(path)) {
|
if (!PROTECTED.includes(path)) {
|
||||||
|
|
||||||
console.log('skip', path)
|
|
||||||
return await ctx.next()
|
return await ctx.next()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,14 +81,22 @@ export async function authMiddleware(req: Request, ctx: CratesCtx): Promise<Resp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify session
|
// Verify session using getSession helper
|
||||||
const session = sessions.get(sessionId)
|
const session = await getSession(sessionId);
|
||||||
|
|
||||||
if (!session || new Date() > session.expiresAt) {
|
if (!session) {
|
||||||
if (session) {
|
if (path.startsWith("/api/")) {
|
||||||
sessions.delete(sessionId)
|
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
|
// Get user from database
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FreshContext } from "$fresh/server.ts"
|
import { FreshContext } from "$fresh/server.ts"
|
||||||
|
import { AppError } from "./errors.ts"
|
||||||
|
|
||||||
export async function errorMiddleware(req: Request, ctx: FreshContext): Promise<Response> {
|
export async function errorMiddleware(req: Request, ctx: FreshContext): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
@@ -6,5 +7,28 @@ export async function errorMiddleware(req: Request, ctx: FreshContext): Promise<
|
|||||||
return response
|
return response
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error handling ${req.method} ${new URL(req.url).pathname}:`, 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,
|
"lock": false,
|
||||||
|
|
||||||
|
"unstable": ["kv"],
|
||||||
|
|
||||||
"tasks": {
|
"tasks": {
|
||||||
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
|
||||||
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
|
"cli": "echo \"import '\\$fresh/src/dev/cli.ts'\" | deno run --unstable -A -",
|
||||||
"manifest": "deno task cli manifest $(pwd)",
|
"manifest": "deno task cli manifest $(pwd)",
|
||||||
"start": "deno run -A --watch=routes/,.env dev.ts",
|
"start": "deno run -A --unstable-kv --watch=routes/,.env dev.ts",
|
||||||
"build": "deno run -A dev.ts build",
|
"build": "deno run -A --unstable-kv dev.ts build",
|
||||||
"preview": "deno run -A main.ts",
|
"preview": "deno run -A main.ts",
|
||||||
"prisma-clean": "rm -rf prisma/generated",
|
"prisma-clean": "rm -rf prisma/generated",
|
||||||
"prisma-generate": "deno task prisma-clean && deno run -A --env-file npm:prisma generate",
|
"prisma-generate": "deno task prisma-clean && deno run -A --env-file npm:prisma generate",
|
||||||
@@ -28,6 +31,8 @@
|
|||||||
"less": "npm:less@4.2.0",
|
"less": "npm:less@4.2.0",
|
||||||
"lucide-preact": "npm:lucide-preact@^0.475.0",
|
"lucide-preact": "npm:lucide-preact@^0.475.0",
|
||||||
"@prisma/client": "npm:@prisma/client@^6.6.0",
|
"@prisma/client": "npm:@prisma/client@^6.6.0",
|
||||||
|
"zodix": "npm:zodix@^0.4.4",
|
||||||
|
|
||||||
|
|
||||||
"api/": "./api/",
|
"api/": "./api/",
|
||||||
"api": "./api/",
|
"api": "./api/",
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import * as $_404 from './routes/_404.tsx'
|
|||||||
import * as $_500 from './routes/_500.tsx'
|
import * as $_500 from './routes/_500.tsx'
|
||||||
import * as $_app from './routes/_app.tsx'
|
import * as $_app from './routes/_app.tsx'
|
||||||
import * as $_middleware from './routes/_middleware.ts'
|
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_login from './routes/api/auth/login.ts'
|
||||||
import * as $api_auth_logout from './routes/api/auth/logout.ts'
|
import * as $api_auth_logout from './routes/api/auth/logout.ts'
|
||||||
import * as $api_auth_register from './routes/api/auth/register.ts'
|
import * as $api_auth_register from './routes/api/auth/register.ts'
|
||||||
@@ -25,7 +24,6 @@ const manifest = {
|
|||||||
'./routes/_500.tsx': $_500,
|
'./routes/_500.tsx': $_500,
|
||||||
'./routes/_app.tsx': $_app,
|
'./routes/_app.tsx': $_app,
|
||||||
'./routes/_middleware.ts': $_middleware,
|
'./routes/_middleware.ts': $_middleware,
|
||||||
'./routes/api/_middleware.ts': $api_middleware,
|
|
||||||
'./routes/api/auth/login.ts': $api_auth_login,
|
'./routes/api/auth/login.ts': $api_auth_login,
|
||||||
'./routes/api/auth/logout.ts': $api_auth_logout,
|
'./routes/api/auth/logout.ts': $api_auth_logout,
|
||||||
'./routes/api/auth/register.ts': $api_auth_register,
|
'./routes/api/auth/register.ts': $api_auth_register,
|
||||||
|
|||||||
+2
-1
@@ -5,6 +5,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
"lodash": "^4.17.21",
|
"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 { serviceMiddleware } from 'api/mw.service.ts'
|
||||||
import { authMiddleware } from 'api/mw.auth.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 { setCookie } from "$std/http/cookie.ts"
|
||||||
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"
|
import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"
|
||||||
|
|
||||||
import { createPg } from "api/pg.ts"
|
import { z } from "npm:zod"
|
||||||
import { sessions } from "api/mw.auth.ts"
|
import { zx } from "npm:zodix"
|
||||||
|
|
||||||
export const handler: Handlers = {
|
import { createSession } from "api/mw.auth.ts"
|
||||||
async POST(req, _ctx) {
|
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 {
|
try {
|
||||||
const data = await req.json()
|
const { email, password } = await zx.parseForm(req, LoginSchema)
|
||||||
const { email, password } = data
|
|
||||||
|
|
||||||
if (!email || !password) {
|
|
||||||
return new Response(JSON.stringify({ error: "Email and password required" }), {
|
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create pg instance for database operations
|
// Create pg instance for database operations
|
||||||
const pg = createPg(console)
|
const { pg } = ctx.state
|
||||||
|
|
||||||
// Find user by email
|
// Find user by email
|
||||||
const user = await pg.userFindEmail(email)
|
const user = await pg.userFindEmail(email)
|
||||||
|
if (!user) throw new InvalidCredentialsError()
|
||||||
if (!user) {
|
|
||||||
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify password
|
// Verify password
|
||||||
const match = await bcrypt.compare(password, user.password)
|
const match = await bcrypt.compare(password, user.password)
|
||||||
|
if (!match) throw new InvalidCredentialsError()
|
||||||
|
|
||||||
if (!match) {
|
// Create session using Deno KV
|
||||||
return new Response(JSON.stringify({ error: "Invalid credentials" }), {
|
const { sessionId, expiresAt } = await createSession(user.id, 24 * 7) // 7 days in hours
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set cookie
|
// Set cookie
|
||||||
const headers = new Headers({ "Content-Type": "application/json" })
|
const headers = new Headers({ "Content-Type": "application/json" })
|
||||||
@@ -71,11 +58,14 @@ export const handler: Handlers = {
|
|||||||
headers
|
headers
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Let the error middleware handle custom errors
|
||||||
|
if (error instanceof ValidationError ||
|
||||||
|
error instanceof InvalidCredentialsError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
console.error("Login error:", error)
|
console.error("Login error:", error)
|
||||||
return new Response(JSON.stringify({ error: "An error occurred" }), {
|
throw new ServerError("An error occurred during login")
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -86,10 +76,7 @@ export const handler: Handlers = {
|
|||||||
const user = ctx.state.user
|
const user = ctx.state.user
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return new Response(JSON.stringify({ error: "Not logged in" }), {
|
throw new NotLoggedInError()
|
||||||
status: 401,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify({
|
return new Response(JSON.stringify({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Handlers } from "$fresh/server.ts"
|
import { Handlers } from "$fresh/server.ts"
|
||||||
import { getCookies, setCookie } from "$std/http/cookie.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 = {
|
export const handler: Handlers = {
|
||||||
async POST(req, _ctx) {
|
async POST(req, _ctx) {
|
||||||
@@ -8,8 +8,8 @@ export const handler: Handlers = {
|
|||||||
const sessionId = cookies.session
|
const sessionId = cookies.session
|
||||||
|
|
||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
// Remove session from storage
|
// Remove session from Deno KV
|
||||||
sessions.delete(sessionId)
|
await deleteSession(sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the cookie
|
// Clear the cookie
|
||||||
|
|||||||
@@ -1,21 +1,36 @@
|
|||||||
import { Handlers } from "$fresh/server.ts"
|
import { Handlers } from "$fresh/server.ts"
|
||||||
import { setCookie } from "$std/http/cookie.ts"
|
import { setCookie } from "$std/http/cookie.ts"
|
||||||
import { createPg } from "../../../api/pg.ts"
|
import { z } from "https://deno.land/x/zod@v3.22.4/mod.ts"
|
||||||
import { sessions } from "../../../api/mw.auth.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 = {
|
export const handler: Handlers = {
|
||||||
async POST(req, _ctx) {
|
async POST(req, _ctx) {
|
||||||
try {
|
try {
|
||||||
const data = await req.json()
|
const data = await req.formData()
|
||||||
const { discogs, email, password } = data
|
|
||||||
|
|
||||||
if (!email || !password) {
|
// Validate input data with Zod
|
||||||
return new Response(JSON.stringify({ error: "Email and password required" }), {
|
const result = RegisterSchema.safeParse(data)
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
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
|
// Create pg instance for database operations
|
||||||
const pg = createPg(console)
|
const pg = createPg(console)
|
||||||
|
|
||||||
@@ -23,25 +38,15 @@ export const handler: Handlers = {
|
|||||||
const existingUser = await pg.userFindEmail(email)
|
const existingUser = await pg.userFindEmail(email)
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return new Response(JSON.stringify({ error: "Email already registered" }), {
|
throw new ResourceExistsError("User with this email")
|
||||||
status: 400,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
const params = { discogs, email, password, data: {} }
|
const params = { discogs, email, password, data: {} }
|
||||||
const user = await pg.userCreate(params)
|
const user = await pg.userCreate(params)
|
||||||
|
|
||||||
// Create session
|
// Create session using Deno KV
|
||||||
const sessionId = crypto.randomUUID()
|
const { sessionId, expiresAt } = await createSession(user.id, 24 * 7) // 7 days in hours
|
||||||
const expiresAt = new Date()
|
|
||||||
expiresAt.setDate(expiresAt.getDate() + 7) // Session expires in 7 days
|
|
||||||
|
|
||||||
sessions.set(sessionId, {
|
|
||||||
userId: user.id,
|
|
||||||
expiresAt
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set cookie
|
// Set cookie
|
||||||
const headers = new Headers({ "Content-Type": "application/json" })
|
const headers = new Headers({ "Content-Type": "application/json" })
|
||||||
@@ -64,11 +69,14 @@ export const handler: Handlers = {
|
|||||||
headers
|
headers
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Let the error middleware handle custom errors
|
||||||
|
if (error instanceof ValidationError ||
|
||||||
|
error instanceof ResourceExistsError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
console.error("Registration error:", error)
|
console.error("Registration error:", error)
|
||||||
return new Response(JSON.stringify({ error: "An error occurred" }), {
|
throw new ServerError("An error occurred during registration")
|
||||||
status: 500,
|
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+10
-5
@@ -1,11 +1,16 @@
|
|||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { createPg, Pg } from "../api/pg.ts";
|
|
||||||
import Main from "../components/main.tsx";
|
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 = {
|
interface HomeData {
|
||||||
async GET(_req, ctx: CratesCtx) {
|
wants: Want[],
|
||||||
|
offers: Offer[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handler: Handlers<HomeData, CratesState> = {
|
||||||
|
async GET(_req, ctx) {
|
||||||
const { pg, offers, user } = ctx.state
|
const { pg, offers, user } = ctx.state
|
||||||
|
|
||||||
console.log(user)
|
console.log(user)
|
||||||
@@ -13,7 +18,7 @@ export const handler: Handlers = {
|
|||||||
const wants = await pg.userWants(user);
|
const wants = await pg.userWants(user);
|
||||||
const uOffers = await offers.userOffers(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 { 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 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 }) {
|
function Login({ error }: { error?: string }) {
|
||||||
const errorEl = error
|
const errorEl = error
|
||||||
@@ -109,7 +9,7 @@ function Login({ error }: { error?: string }) {
|
|||||||
return (
|
return (
|
||||||
<div class="login">
|
<div class="login">
|
||||||
{errorEl}
|
{errorEl}
|
||||||
<form method="POST" action="/login">
|
<form method="POST" action="/api/auth/login">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="email">email</label>
|
<label for="email">email</label>
|
||||||
<input type="email" name="email" id="email" required />
|
<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 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 }) {
|
function Register({ error = "" }: { error?: string }) {
|
||||||
|
|
||||||
@@ -112,7 +9,7 @@ function Register({ error = "" }: { error?: string }) {
|
|||||||
return (
|
return (
|
||||||
<div class="login">
|
<div class="login">
|
||||||
{errorEl}
|
{errorEl}
|
||||||
<form action="/register" method="post">
|
<form action="/api/auth/register" method="post">
|
||||||
<div class="item">
|
<div class="item">
|
||||||
<label for="email">email</label>
|
<label for="email">email</label>
|
||||||
<input type="email" name="email" id="email" required />
|
<input type="email" name="email" id="email" required />
|
||||||
|
|||||||
Reference in new issue
Block a user