diff --git a/astro.config.mjs b/astro.config.ts similarity index 91% rename from astro.config.mjs rename to astro.config.ts index 7a6d072..ae7e71d 100644 --- a/astro.config.mjs +++ b/astro.config.ts @@ -10,6 +10,7 @@ import icon from "astro-icon"; import { defineConfig, envField } from "astro/config"; import { viteStaticCopy } from "vite-plugin-static-copy"; import { version } from "./package.json"; +import { parsedDoc } from "./server/config.js"; export default defineConfig({ experimental: { env: { @@ -19,6 +20,12 @@ export default defineConfig({ access: "public", optional: true, default: version + }), + MARKETPLACE_ENABLED: envField.boolean({ + context: 'client', + access: 'public', + optional: true, + default: parsedDoc.marketplace.enabled }) } } diff --git a/server/dbSetup.ts b/server/dbSetup.ts index e3d3ddf..3a6e047 100644 --- a/server/dbSetup.ts +++ b/server/dbSetup.ts @@ -2,7 +2,7 @@ import { fileURLToPath } from "node:url"; import chalk from "chalk"; import ora from "ora"; import { ModelStatic } from "sequelize"; -import { Catalog, CatalogModel } from "./server.js"; +import { Catalog, CatalogModel } from "./marketplace.js"; interface Items extends Omit { background_video?: string; diff --git a/server/marketplace.ts b/server/marketplace.ts new file mode 100644 index 0000000..ede97bc --- /dev/null +++ b/server/marketplace.ts @@ -0,0 +1,225 @@ +import { Sequelize, Model, InferAttributes, InferCreationAttributes, DataTypes } from "sequelize"; +import { parsedDoc } from "./config.js"; +import { FastifyInstance, FastifyRequest } from "fastify"; +import { fileURLToPath } from "node:url"; +import { pipeline } from "node:stream/promises"; +import { createWriteStream } from "node:fs"; +import { access, mkdir, constants } from "node:fs/promises"; + +const db = new Sequelize(parsedDoc.db.name, parsedDoc.db.username, parsedDoc.db.password, { + host: parsedDoc.db.postgres ? `${parsedDoc.postgres.domain}` : "localhost", + port: parsedDoc.db.postgres ? parsedDoc.postgres.port : undefined, + dialect: parsedDoc.db.postgres ? "postgres" : "sqlite", + logging: parsedDoc.server.server.logging, + storage: "database.sqlite" //this is sqlite only +}); + +type CatalogType = "theme" | "plugin"; + +interface Catalog { + package_name: string; + title: string; + description: string; + author: string; + image: string; + tags: object; + version: string; + background_image: string; + background_video: string; + payload: string; + type: CatalogType; +} + +interface CatalogModel + extends Catalog, + Model, InferCreationAttributes> {} + +const catalogAssets = db.define("catalog_assets", { + package_name: { type: DataTypes.STRING, unique: true }, + title: { type: DataTypes.TEXT }, + description: { type: DataTypes.TEXT }, + author: { type: DataTypes.TEXT }, + image: { type: DataTypes.TEXT }, + tags: { type: DataTypes.JSON, allowNull: true }, + version: { type: DataTypes.TEXT }, + background_image: { type: DataTypes.TEXT, allowNull: true }, + background_video: { type: DataTypes.TEXT, allowNull: true }, + payload: { type: DataTypes.TEXT }, + type: { type: DataTypes.TEXT } +}); + +function marketplaceAPI(app: FastifyInstance) { + app.get("/api", (request, reply) => { + reply.send({ Server: "Active" }); + }); + + // This API returns a list of the assets in the database (SW plugins and themes). + // It also returns the number of pages in the database. + // It can take a `?page=x` argument to display a different page, with a limit of 20 assets per page. + type CatalogAssetsReq = FastifyRequest<{ Querystring: { page: string } }>; + app.get("/api/catalog-assets/", async (request: CatalogAssetsReq, reply) => { + try { + const { page } = request.query; + const pageNum: number = parseInt(page, 10) || 1; + if (pageNum < 1) { + reply.status(400).send({ error: "Page must be a positive number!" }); + } + const offset = (pageNum - 1) * 20; + const totalItems = await catalogAssets.count(); + const dbAssets = await catalogAssets.findAll({ offset: offset, limit: 20 }); + const assets = dbAssets.reduce((acc, asset) => { + acc[asset.package_name] = { + title: asset.title, + description: asset.description, + author: asset.author, + image: asset.image, + tags: asset.tags, + version: asset.version, + background_image: asset.background_image, + background_video: asset.background_video, + payload: asset.payload, + type: asset.type + }; + return acc; + }, {}); + return reply.send({ assets, pages: Math.ceil(totalItems / 20) }); + } catch (error) { + return reply.status(500).send({ error: "An error occured" }); + } + }); + + type PackageReq = FastifyRequest<{ Params: { package: string } }>; + app.get("/api/packages/:package", async (request: PackageReq, reply) => { + try { + const packageRow = await catalogAssets.findOne({ + where: { package_name: request.params.package } + }); + if (!packageRow) return reply.status(404).send({ error: "Package not found!" }); + const details = { + title: packageRow.get("title"), + description: packageRow.get("description"), + image: packageRow.get("image"), + author: packageRow.get("author"), + tags: packageRow.get("tags"), + version: packageRow.get("version"), + background_image: packageRow.get("background_image"), + background_video: packageRow.get("background_video"), + payload: packageRow.get("payload"), + type: packageRow.get("type") + }; + reply.send(details); + } catch (error) { + reply.status(500).send({ error: "An unexpected error occured" }); + } + }); + + type UploadReq = FastifyRequest<{ Headers: { psk: string; packagename: string } }>; + type CreateReq = FastifyRequest<{ + Headers: { psk: string }; + Body: { + uuid: string; + title: string; + image: string; + author: string; + version: string; + description: string; + tags: object | any; + payload: string; + background_video: string; + background_image: string; + type: CatalogType; + }; + }>; + interface VerifyStatus { + status: number; + error?: Error; + } + async function verifyReq( + request: UploadReq | CreateReq, + upload: Boolean, + data: any + ): Promise { + if (parsedDoc.marketplace.enabled === false) { + return { status: 500, error: new Error("Marketplace Is disabled!") }; + } else if (request.headers.psk !== parsedDoc.marketplace.psk) { + return { status: 403, error: new Error("PSK isn't correct!") }; + } else if (upload && !request.headers.packagename) { + return { status: 500, error: new Error("No packagename defined!") }; + } else if (upload && !data) { + return { status: 400, error: new Error("No file uploaded!") }; + } else { + return { status: 200 }; + } + } + + app.post("/api/upload-asset", async (request: UploadReq, reply) => { + const data = await request.file(); + const verify: VerifyStatus = await verifyReq(request, true, data); + if (verify.error !== undefined) { + reply.status(verify.status).send({ status: verify.error.message }); + } else { + try { + await pipeline( + data.file, + createWriteStream( + fileURLToPath( + new URL( + `../database_assets/${request.headers.packagename}/${data.filename}`, + import.meta.url + ) + ) + ) + ); + } catch (error) { + return reply + .status(500) + .send({ status: `File couldn't be uploaded! (Package most likely doesn't exist)` }); + } + return reply.status(verify.status).send({ status: "File uploaded successfully!" }); + } + }); + + app.post("/api/create-package", async (request: CreateReq, reply) => { + const verify: VerifyStatus = await verifyReq(request, false, undefined); + if (verify.error !== undefined) { + reply.status(verify.status).send({ status: verify.error.message }); + } else { + const body: Catalog = { + package_name: request.body.uuid, + title: request.body.title, + image: request.body.image, + author: request.body.author, + version: request.body.version, + description: request.body.description, + tags: request.body.tags, + payload: request.body.payload, + background_video: request.body.background_video, + background_image: request.body.background_image, + type: request.body.type as CatalogType + }; + await catalogAssets.create({ + package_name: body.package_name, + title: body.title, + image: body.image, + author: body.author, + version: body.version, + description: body.description, + tags: body.tags, + payload: body.payload, + background_video: body.background_video, + background_image: body.background_image, + type: body.type + }); + const assets = fileURLToPath(new URL("../database_assets", import.meta.url)); + try { + await access(`${assets}/${body.package_name}/`, constants.F_OK); + return reply.status(500).send({ status: "Package already exists!" }); + } catch (err) { + await mkdir(`${assets}/${body.package_name}/`); + return reply.status(verify.status).send({ status: "Package created successfully!" }); + } + } + }); +} + +export { marketplaceAPI, db, catalogAssets, Catalog, CatalogModel } diff --git a/server/server.ts b/server/server.ts index 8b88a6d..016fdbe 100644 --- a/server/server.ts +++ b/server/server.ts @@ -14,6 +14,7 @@ import { handler as ssrHandler } from "../dist/server/entry.mjs"; import { parsedDoc } from "./config.js"; import { setupDB } from "./dbSetup.js"; import { serverFactory } from "./serverFactory.js"; +import { marketplaceAPI, catalogAssets } from "./marketplace.js"; const app = Fastify({ logger: parsedDoc.server.server.logging, @@ -21,47 +22,6 @@ const app = Fastify({ ignoreTrailingSlash: true, serverFactory: serverFactory }); -const db = new Sequelize(parsedDoc.db.name, parsedDoc.db.username, parsedDoc.db.password, { - host: parsedDoc.db.postgres ? `${parsedDoc.postgres.domain}` : "localhost", - port: parsedDoc.db.postgres ? parsedDoc.postgres.port : undefined, - dialect: parsedDoc.db.postgres ? "postgres" : "sqlite", - logging: parsedDoc.server.server.logging, - storage: "database.sqlite" //this is sqlite only -}); - -type CatalogType = "theme" | "plugin"; - -interface Catalog { - package_name: string; - title: string; - description: string; - author: string; - image: string; - tags: object; - version: string; - background_image: string; - background_video: string; - payload: string; - type: CatalogType; -} - -interface CatalogModel - extends Catalog, - Model, InferCreationAttributes> {} - -const catalogAssets = db.define("catalog_assets", { - package_name: { type: DataTypes.STRING, unique: true }, - title: { type: DataTypes.TEXT }, - description: { type: DataTypes.TEXT }, - author: { type: DataTypes.TEXT }, - image: { type: DataTypes.TEXT }, - tags: { type: DataTypes.JSON, allowNull: true }, - version: { type: DataTypes.TEXT }, - background_image: { type: DataTypes.TEXT, allowNull: true }, - background_video: { type: DataTypes.TEXT, allowNull: true }, - payload: { type: DataTypes.TEXT }, - type: { type: DataTypes.TEXT } -}); await app.register(fastifyCompress, { encodings: ["br", "gzip", "deflate"] @@ -74,186 +34,18 @@ await app.register(fastifyStatic, { decorateReply: false }); -await app.register(fastifyStatic, { - root: fileURLToPath(new URL("../database_assets", import.meta.url)), - prefix: "/packages/", - decorateReply: false -}); +//Our marketplace API. Not middleware as I don't want to deal with that LOL. Just a function that passes our app to it. +if (parsedDoc.marketplace.enabled) { + await app.register(fastifyStatic, { + root: fileURLToPath(new URL("../database_assets", import.meta.url)), + prefix: "/packages/", + decorateReply: false + }); + marketplaceAPI(app); +} await app.register(fastifyMiddie); -app.get("/api", (request, reply) => { - reply.send({ Server: "Active" }); -}); - -// This API returns a list of the assets in the database (SW plugins and themes). -// It also returns the number of pages in the database. -// It can take a `?page=x` argument to display a different page, with a limit of 20 assets per page. -type CatalogAssetsReq = FastifyRequest<{ Querystring: { page: string } }>; -app.get("/api/catalog-assets/", async (request: CatalogAssetsReq, reply) => { - try { - const { page } = request.query; - const pageNum: number = parseInt(page, 10) || 1; - if (pageNum < 1) { - reply.status(400).send({ error: "Page must be a positive number!" }); - } - const offset = (pageNum - 1) * 20; - const totalItems = await catalogAssets.count(); - const dbAssets = await catalogAssets.findAll({ offset: offset, limit: 20 }); - const assets = dbAssets.reduce((acc, asset) => { - acc[asset.package_name] = { - title: asset.title, - description: asset.description, - author: asset.author, - image: asset.image, - tags: asset.tags, - version: asset.version, - background_image: asset.background_image, - background_video: asset.background_video, - payload: asset.payload, - type: asset.type - }; - return acc; - }, {}); - return reply.send({ assets, pages: Math.ceil(totalItems / 20) }); - } catch (error) { - return reply.status(500).send({ error: "An error occured" }); - } -}); - -type PackageReq = FastifyRequest<{ Params: { package: string } }>; -app.get("/api/packages/:package", async (request: PackageReq, reply) => { - try { - const packageRow = await catalogAssets.findOne({ - where: { package_name: request.params.package } - }); - if (!packageRow) return reply.status(404).send({ error: "Package not found!" }); - const details = { - title: packageRow.get("title"), - description: packageRow.get("description"), - image: packageRow.get("image"), - author: packageRow.get("author"), - tags: packageRow.get("tags"), - version: packageRow.get("version"), - background_image: packageRow.get("background_image"), - background_video: packageRow.get("background_video"), - payload: packageRow.get("payload"), - type: packageRow.get("type") - }; - reply.send(details); - } catch (error) { - reply.status(500).send({ error: "An unexpected error occured" }); - } -}); - -type UploadReq = FastifyRequest<{ Headers: { psk: string; packagename: string } }>; -type CreateReq = FastifyRequest<{ - Headers: { psk: string }; - Body: { - uuid: string; - title: string; - image: string; - author: string; - version: string; - description: string; - tags: object | any; - payload: string; - background_video: string; - background_image: string; - type: CatalogType; - }; -}>; -interface VerifyStatus { - status: number; - error?: Error; -} -async function verifyReq( - request: UploadReq | CreateReq, - upload: Boolean, - data: any -): Promise { - if (parsedDoc.marketplace.enabled === false) { - return { status: 500, error: new Error("Marketplace Is disabled!") }; - } else if (request.headers.psk !== parsedDoc.marketplace.psk) { - return { status: 403, error: new Error("PSK isn't correct!") }; - } else if (upload && !request.headers.packagename) { - return { status: 500, error: new Error("No packagename defined!") }; - } else if (upload && !data) { - return { status: 400, error: new Error("No file uploaded!") }; - } else { - return { status: 200 }; - } -} - -app.post("/api/upload-asset", async (request: UploadReq, reply) => { - const data = await request.file(); - const verify: VerifyStatus = await verifyReq(request, true, data); - if (verify.error !== undefined) { - reply.status(verify.status).send({ status: verify.error.message }); - } else { - try { - await pipeline( - data.file, - createWriteStream( - fileURLToPath( - new URL( - `../database_assets/${request.headers.packagename}/${data.filename}`, - import.meta.url - ) - ) - ) - ); - } catch (error) { - return reply - .status(500) - .send({ status: `File couldn't be uploaded! (Package most likely doesn't exist)` }); - } - return reply.status(verify.status).send({ status: "File uploaded successfully!" }); - } -}); - -app.post("/api/create-package", async (request: CreateReq, reply) => { - const verify: VerifyStatus = await verifyReq(request, false, undefined); - if (verify.error !== undefined) { - reply.status(verify.status).send({ status: verify.error.message }); - } else { - const body: Catalog = { - package_name: request.body.uuid, - title: request.body.title, - image: request.body.image, - author: request.body.author, - version: request.body.version, - description: request.body.description, - tags: request.body.tags, - payload: request.body.payload, - background_video: request.body.background_video, - background_image: request.body.background_image, - type: request.body.type as CatalogType - }; - await catalogAssets.create({ - package_name: body.package_name, - title: body.title, - image: body.image, - author: body.author, - version: body.version, - description: body.description, - tags: body.tags, - payload: body.payload, - background_video: body.background_video, - background_image: body.background_image, - type: body.type - }); - const assets = fileURLToPath(new URL("../database_assets", import.meta.url)); - try { - await access(`${assets}/${body.package_name}/`, constants.F_OK); - return reply.status(500).send({ status: "Package already exists!" }); - } catch (err) { - await mkdir(`${assets}/${body.package_name}/`); - return reply.status(verify.status).send({ status: "Package created successfully!" }); - } - } -}); - app.use(ssrHandler); const port: number = @@ -282,8 +74,8 @@ app.listen({ port: port, host: "0.0.0.0" }).then(async () => { `Server also listening on ${chalk.hex("#eb6f92").bold("http://0.0.0.0:" + port + "/")}` ) ); - await catalogAssets.sync(); - await setupDB(catalogAssets); + if (parsedDoc.marketplace.enabled) { + await catalogAssets.sync(); + await setupDB(catalogAssets); + } }); - -export { CatalogModel, Catalog }; diff --git a/src/components/Header.astro b/src/components/Header.astro index 986e3c3..0221bbf 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -4,7 +4,7 @@ import { getLangFromUrl, useTranslations } from "../i18n/utils"; import { isMobileNavOpen } from "../store.js"; import HeaderButton from "./HeaderButton.astro"; import Logo from "./Logo.astro"; - +import { MARKETPLACE_ENABLED } from "astro:env/client"; const lang = getLangFromUrl(Astro.url); const t = useTranslations(lang); --- @@ -40,12 +40,14 @@ const t = useTranslations(lang); class="h-6 w-6 text-text-color transition duration-500 group-hover:text-text-hover-color md:h-6 md:w-6" /> - - - + {MARKETPLACE_ENABLED && + + + + } - - - + {MARKETPLACE_ENABLED && + + + + } - + diff --git a/src/pages/[lang]/settings/appearance.astro b/src/pages/[lang]/settings/appearance.astro index 90897ee..1277b30 100644 --- a/src/pages/[lang]/settings/appearance.astro +++ b/src/pages/[lang]/settings/appearance.astro @@ -12,6 +12,7 @@ export function getStaticPaths() { return STATIC_PATHS; } export const prerender = true; +import { MARKETPLACE_ENABLED } from "astro:env/client"; --- @@ -22,14 +23,16 @@ export const prerender = true;