diff --git a/crates/api/error.middleware.ts b/crates/api/error.middleware.ts deleted file mode 100644 index 4a1fad5..0000000 --- a/crates/api/error.middleware.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { FreshContext } from "$fresh/server.ts" - -export async function errorMiddleware(req: Request, ctx: FreshContext): Promise { - try { - const response = await ctx.next() - return response - } catch (error) { - console.error(`Error handling ${req.method} ${new URL(req.url).pathname}:`, error) - - // Check if this is an API route - const isApiRoute = new URL(req.url).pathname.startsWith("/api/") - - if (isApiRoute) { - return new Response(JSON.stringify({ - error: "Internal server error", - message: error.message - }), { - status: 500, - headers: { "Content-Type": "application/json" } - }) - } - - // For non-API routes, return a redirect to an error page or render an error component - return new Response(null, { - status: 302, - headers: { Location: "/?error=server_error" } - }) - } -} \ No newline at end of file diff --git a/crates/api/auth.middleware.ts b/crates/api/mw.auth.ts similarity index 83% rename from crates/api/auth.middleware.ts rename to crates/api/mw.auth.ts index 0a501ef..adfac86 100644 --- a/crates/api/auth.middleware.ts +++ b/crates/api/mw.auth.ts @@ -1,30 +1,33 @@ import { FreshContext } from "$fresh/server.ts" -import { getCookies, setCookie } from "$std/http/cookie.ts" -import { createPg } from "api/pg.ts" +import { getCookies } from "$std/http/cookie.ts" +import { CratesCtx } from 'api/mw.service.ts' -// Define public paths that don't require authentication const PROTECTED = [ - "/home", "/api/auth", + "/home", ] // Sessions storage shared between routes // In production, this would be Redis or a database export const sessions = new Map() -export async function authMiddleware(req: Request, ctx: FreshContext): Promise { +export async function authMiddleware(req: Request, ctx: CratesCtx): Promise { + const { pg } = ctx.state + const url = new URL(req.url) const path = url.pathname - // Allow public paths without authentication - if (!PROTECTED.includes(path)) { - return await ctx.next() - } - // Check for authentication const cookies = getCookies(req.headers) const sessionId = cookies.session + // Allow public paths without authentication + if (!PROTECTED.includes(path)) { + + console.log('skip', path) + return await ctx.next() + } + if (!sessionId) { if (path.startsWith("/api/")) { return new Response(JSON.stringify({ error: "Not logged in" }), { @@ -51,7 +54,6 @@ export async function authMiddleware(req: Request, ctx: FreshContext): Promise { + try { + const response = await ctx.next() + return response + } catch (error) { + console.error(`Error handling ${req.method} ${new URL(req.url).pathname}:`, error) + } +} \ No newline at end of file diff --git a/crates/api/mw.service.ts b/crates/api/mw.service.ts new file mode 100755 index 0000000..3076e88 --- /dev/null +++ b/crates/api/mw.service.ts @@ -0,0 +1,25 @@ +import { FreshContext } from "$fresh/server.ts"; +import { User } from 'prisma/generated/index.ts' + +import { Pg } from 'api/pg.ts' +import { Offers } from 'api/offers.ts' +import { createPg } from 'api/pg.ts' +import createOffers from 'api/offers.ts' + +export interface CratesState { + user: User + pg: Pg; + offers: Offers +} + +export type CratesCtx = FreshContext + +export async function serviceMiddleware(_req: Request, ctx: FreshContext): Promise { + const pg = createPg(console) + const offers = createOffers(pg) + + ctx.state.pg = pg + ctx.state.offers = offers + + return await ctx.next() +} \ No newline at end of file diff --git a/crates/api/offers.ts b/crates/api/offers.ts index 63cc529..b626f5f 100644 --- a/crates/api/offers.ts +++ b/crates/api/offers.ts @@ -1,13 +1,9 @@ -import type Log from 'bunyan'; -import type { PrismaClient, Want, User, Listing, Offer, } from '@prisma/client'; -import assert from 'assert/strict'; -import groupBy from 'lodash/groupBy'; -import sortBy from 'lodash/sortBy'; -import flatMap from 'lodash/flatMap'; -import { Context } from 'koa'; +import { assert } from "$std/assert/mod.ts"; +import { groupBy, sortBy, flatMap } from 'lodash' -import { logAxiosError, wantDetails } from './utils'; -import { Pg } from './pg'; +import type { User } from 'prismaClient'; +import { Pg } from "api/pg.ts"; +import { UUID } from 'node:crypto' export enum Status { Complete = 'Complete', @@ -21,10 +17,10 @@ function sortOffers(o) { return Object.values(Status).indexOf(o.status); } -export default function createOffers(log: Log, pg: Pg) { +export default function createOffers(pg: Pg) { async function generate() { const listings = flatMap(await pg.interestingListings()); - log.debug('generateOffers', { listings: listings.length }); + console.debug('generateOffers', { listings: listings.length }); const shipping = 7.5; // yolt @@ -51,30 +47,35 @@ export default function createOffers(log: Log, pg: Pg) { // TODO probably need to take one of each condition of record from a store // eg. they have a VG+ and a M we should make offers for each - log.trace('generated offers', sorted); + console.trace('generated offers', sorted); const inserts = await pg.offersUpdate(sorted); return inserts; } - async function accept(ctx: Context) { - const { id } = ctx.request.body; - ctx.assert(id, 401, 'no id supplied'); + async function accept(user: User, id: UUID) { + assert(id, 'no id supplied'); // check the user actually has this offer - const offer = await pg.userOfferFind(ctx.state.user, id); + const offer = await pg.userOfferFind(user, id); return pg.offerStatusSet([offer.id], Status.Accepted); } async function statusSet(user: User, ids: UUID[], status: Status) { - log.info({ status, ids }, 'setting offer status'); + console.info({ status, ids }, 'setting offer status'); + + // Validate the inputs + assert(Array.isArray(ids), "ids must be an array"); + assert(ids.length > 0, "ids array cannot be empty"); + assert(ids.every(id => isUUID(id)), "All ids must be valid UUIDs"); + assert(Object.values(Status).includes(status), "Invalid status value"); // const offer = await pg.offerFindId(ids); const update = await pg.offerStatusSet(ids, status); // forget about this for now // if (status === Status.Complete) { - // log.info({ id, status }, 'completed offer, removing want...'); + // console.info({ id, status }, 'completed offer, removing want...'); // await pg.wantRm(offer.wantId); // } diff --git a/crates/api/pg.ts b/crates/api/pg.ts index 34078b2..12980cc 100644 --- a/crates/api/pg.ts +++ b/crates/api/pg.ts @@ -1,6 +1,6 @@ import * as bcrypt from "https://deno.land/x/bcrypt@v0.4.1/mod.ts" import { load } from "$std/dotenv/mod.ts" -import { PrismaClient } from "prisma/generated/index.ts"; +import { PrismaClient } from "prismaClient" // Load environment variables await load({ export: true }) diff --git a/crates/api/utils.ts b/crates/api/utils.ts new file mode 100755 index 0000000..73fdabd --- /dev/null +++ b/crates/api/utils.ts @@ -0,0 +1,34 @@ +import type { Want, User } from "prismaClient"; + +export function logFetchError(e: Response, requestUrl: string, responseData: any) { + console.warn({ + status: e.status, + statusText: e.statusText, + url: requestUrl, + data: responseData, + rateLimiting: { + limit: e.headers.get('x-discogs-ratelimit'), + remaining: e.headers.get('x-discogs-ratelimit-remaining'), + used: e.headers.get('x-discogs-ratelimit-used'), + }, + }); +} + +export function wantDetails(want: Want) { + const data = want.data as any; + + if (!data) console.warn(want); + + return { + title: data.title, + release: data.id, + artist: data.artists_sort, + id: want.id + } +} + +export function isAdmin(user: User) { + return user.email === 'ntr@mnml.gg'; +} + +export function offerDetails() {} diff --git a/crates/static/style/fonts.less b/crates/components/style/fonts.less similarity index 100% rename from crates/static/style/fonts.less rename to crates/components/style/fonts.less diff --git a/crates/static/style/index.less b/crates/components/style/index.less similarity index 100% rename from crates/static/style/index.less rename to crates/components/style/index.less diff --git a/crates/static/style/styles.less b/crates/components/style/styles.less similarity index 100% rename from crates/static/style/styles.less rename to crates/components/style/styles.less diff --git a/crates/crates.sublime-workspace b/crates/crates.sublime-workspace index 0d240a3..fc0f21f 100644 --- a/crates/crates.sublime-workspace +++ b/crates/crates.sublime-workspace @@ -936,7 +936,7 @@ ] }, { - "file": "api/auth.middleware.ts", + "file": "api/mw.auth.ts", "settings": { "buffer_size": 2440, @@ -1546,7 +1546,7 @@ [ "/home/ntr/crates/crates/deno.jsonc", "/home/ntr/crates/crates/crates.sublime-project", - "/home/ntr/crates/crates/api/auth.middleware.ts", + "/home/ntr/crates/crates/api/mw.auth.ts", "/home/ntr/crates/crates/import_map.json", "/home/ntr/crates/crates/api/pg.ts", "/home/ntr/crates/crates.sublime-project", @@ -1816,7 +1816,7 @@ }, { "buffer": 2, - "file": "api/auth.middleware.ts", + "file": "api/mw.auth.ts", "semi_transient": false, "settings": { @@ -1850,7 +1850,7 @@ } ], "lsp_active": true, - "lsp_uri": "file:///home/ntr/crates/crates/api/auth.middleware.ts", + "lsp_uri": "file:///home/ntr/crates/crates/api/mw.auth.ts", "show_definitions": false, "syntax": "Packages/JavaScript/TypeScript.sublime-syntax", "tab_size": 4, @@ -1941,7 +1941,7 @@ ], [ "middle", - "api/auth.middleware.ts" + "api/mw.auth.ts" ], [ "pg", diff --git a/crates/deno.jsonc b/crates/deno.jsonc index f882593..a490a12 100644 --- a/crates/deno.jsonc +++ b/crates/deno.jsonc @@ -4,7 +4,7 @@ "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=static/,routes/ dev.ts", + "start": "deno run -A --watch=routes/,.env dev.ts", "build": "deno run -A dev.ts build", "preview": "deno run -A main.ts", "prisma-clean": "rm -rf prisma/generated", @@ -24,6 +24,7 @@ "@twind/preset-tailwind": "https://esm.sh/@twind/preset-tailwind@1.1.4/", "@twind/preset-autoprefix": "https://esm.sh/@twind/preset-autoprefix@1.0.7/", + "lodash": "https://deno.land/x/lodash@4.17.15-es/lodash.js", "less": "npm:less@4.2.0", "lucide-preact": "npm:lucide-preact@^0.475.0", "@prisma/client": "npm:@prisma/client@^6.6.0", @@ -31,7 +32,8 @@ "api/": "./api/", "api": "./api/", "prisma/": "./prisma/", - "prisma": "./prisma/" + "prisma": "./prisma/", + "prismaClient": "./prisma/generated/index.ts", }, "fmt": { diff --git a/crates/fresh.config.ts b/crates/fresh.config.ts index da843ed..b22e1e8 100644 --- a/crates/fresh.config.ts +++ b/crates/fresh.config.ts @@ -3,6 +3,9 @@ import { defineConfig } from "$fresh/server.ts"; // import twindConfig from "./twind.config.ts"; import lessPlugin from "./less.plugin.ts"; +const less = lessPlugin() +await less.buildStart() + export default defineConfig({ plugins: [ // twind(twindConfig), diff --git a/crates/fresh.gen.ts b/crates/fresh.gen.ts index 69db3b5..bf81e7b 100644 --- a/crates/fresh.gen.ts +++ b/crates/fresh.gen.ts @@ -3,8 +3,10 @@ // This file is automatically updated during development when running `dev.ts`. 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' @@ -20,8 +22,10 @@ import type { Manifest } from '$fresh/server.ts' const manifest = { routes: { './routes/_404.tsx': $_404, + './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, diff --git a/crates/less.plugin.ts b/crates/less.plugin.ts index 0806f87..e9d6124 100644 --- a/crates/less.plugin.ts +++ b/crates/less.plugin.ts @@ -1,5 +1,5 @@ import { Plugin } from "$fresh/server.ts"; -import { ensureDir, exists } from "$std/fs/mod.ts"; +import { ensureDir } from "$std/fs/mod.ts"; import { join } from "$std/path/mod.ts"; import { dirname } from "$std/path/mod.ts"; @@ -12,7 +12,7 @@ interface LessPluginOptions { } export default function lessPlugin(): Plugin { - const path = "static/style" + const path = "components/style" const outDir = "static/css" // Compile the Less file using the Less package @@ -47,13 +47,5 @@ export default function lessPlugin(): Plugin { await compileLess(inputPath, outputPath); console.log("Less styles compiled during build start"); }, - - // async renderAsync(ctx) { - // await ctx.renderAsync(); - - // // 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 6e5d242..675f529 100644 --- a/crates/main.ts +++ b/crates/main.ts @@ -10,9 +10,4 @@ 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/routes/_500.tsx b/crates/routes/_500.tsx new file mode 100755 index 0000000..6401866 --- /dev/null +++ b/crates/routes/_500.tsx @@ -0,0 +1,28 @@ +import { PageProps } from "$fresh/server.ts"; +import { Head } from "$fresh/runtime.ts"; + +export default function Error500Page({ error }: PageProps) { + return ( + <> + + 500 - Server Error + +
+
+ the Fresh logo: a sliced lemon dripping with juice +

500 - Server Error

+

+ {(error as Error).message} +

+ Go back home +
+
+ + ); +} diff --git a/crates/routes/_middleware.ts b/crates/routes/_middleware.ts index d19c0da..571e878 100644 --- a/crates/routes/_middleware.ts +++ b/crates/routes/_middleware.ts @@ -1,12 +1,5 @@ -import { FreshContext } from "$fresh/server.ts"; -import { Pg } from 'api/pg.ts' -import { User } from 'prisma/generated/index.ts' -import { authMiddleware } from 'api/auth.middleware.ts' -import { errorMiddleware } from 'api/error.middleware.ts' +import { errorMiddleware } from 'api/mw.error.ts' +import { serviceMiddleware } from 'api/mw.service.ts' +import { authMiddleware } from 'api/mw.auth.ts' -interface State { - pg: Pg; - user: User -} - -export const handler = [errorMiddleware, authMiddleware] +export const handler = [serviceMiddleware, authMiddleware] diff --git a/crates/routes/api/_middleware.ts b/crates/routes/api/_middleware.ts new file mode 100755 index 0000000..2e869e7 --- /dev/null +++ b/crates/routes/api/_middleware.ts @@ -0,0 +1,2 @@ +import { authMiddleware } from 'api/mw.auth.ts' +export const handler = [authMiddleware] diff --git a/crates/routes/api/auth/login.ts b/crates/routes/api/auth/login.ts index 641bcbd..9428f59 100644 --- a/crates/routes/api/auth/login.ts +++ b/crates/routes/api/auth/login.ts @@ -3,7 +3,7 @@ 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/auth.middleware.ts" +import { sessions } from "api/mw.auth.ts" export const handler: Handlers = { async POST(req, _ctx) { diff --git a/crates/routes/api/auth/logout.ts b/crates/routes/api/auth/logout.ts index ee71cd3..bcd9594 100644 --- a/crates/routes/api/auth/logout.ts +++ b/crates/routes/api/auth/logout.ts @@ -1,6 +1,6 @@ import { Handlers } from "$fresh/server.ts" import { getCookies, setCookie } from "$std/http/cookie.ts" -import { sessions } from "../../../api/auth.middleware.ts" +import { sessions } from "../../../api/mw.auth.ts" export const handler: Handlers = { async POST(req, _ctx) { diff --git a/crates/routes/api/auth/register.ts b/crates/routes/api/auth/register.ts index 45940f9..d3034e0 100644 --- a/crates/routes/api/auth/register.ts +++ b/crates/routes/api/auth/register.ts @@ -1,7 +1,7 @@ import { Handlers } from "$fresh/server.ts" import { setCookie } from "$std/http/cookie.ts" import { createPg } from "../../../api/pg.ts" -import { sessions } from "../../../api/auth.middleware.ts" +import { sessions } from "../../../api/mw.auth.ts" export const handler: Handlers = { async POST(req, _ctx) { diff --git a/crates/routes/api/offers.ts b/crates/routes/api/offers.ts index ba69dc4..2ddb7d5 100644 --- a/crates/routes/api/offers.ts +++ b/crates/routes/api/offers.ts @@ -1,9 +1,11 @@ import { Handlers } from "$fresh/server.ts" +import * as offersService from "api/offers.ts" + export const handler: Handlers = { async GET(_req, ctx) { const user = ctx.state.user - const offers = await pg.userOffers(user) + const offers = await offersService.userOffers(user) return new Response(JSON.stringify(offers), { status: 200, diff --git a/crates/routes/home.tsx b/crates/routes/home.tsx index 33189f0..b6040e8 100644 --- a/crates/routes/home.tsx +++ b/crates/routes/home.tsx @@ -1,17 +1,19 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { createPg, Pg } from "../api/pg.ts"; import Main from "../components/main.tsx"; -import { User } from 'prisma/generated/index.ts' + +import { CratesCtx } from "api/mw.service.ts" export const handler: Handlers = { - async GET(req, ctx) { - const user: User = ctx.state.user; - const pg: Pg = createPg(console); + async GET(_req, ctx: CratesCtx) { + const { pg, offers, user } = ctx.state + + console.log(user) const wants = await pg.userWants(user); - const offers = await pg.userOffers(user); + const uOffers = await offers.userOffers(user); - return ctx.render({ wants, offers }); + return ctx.render({ wants, offers: uOffers }); }, }; diff --git a/crates/routes/login.tsx b/crates/routes/login.tsx index bab9b2e..2c7d321 100644 --- a/crates/routes/login.tsx +++ b/crates/routes/login.tsx @@ -2,7 +2,7 @@ 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 { 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"; diff --git a/crates/routes/register.tsx b/crates/routes/register.tsx index 4fe7eff..e541094 100644 --- a/crates/routes/register.tsx +++ b/crates/routes/register.tsx @@ -3,7 +3,7 @@ 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 { sessions } from "../api/mw.auth.ts"; import { createPg } from "../api/pg.ts"; export const handler: Handlers = {