diff --git a/env.example b/env.example index ab9652b..d021ae8 100644 --- a/env.example +++ b/env.example @@ -16,3 +16,8 @@ PASSPORT_SUCCESS_REDIRECT_URL=your_success_redirect_url TERMIN_PLANNER_AUTHORIZATION_PASSWORD=your_authorization_password TERMIN_PLANNER_URL=your_termin_planner_url + +WEBSITE_BUILDER_TEMPLATE_REPOSITORY_URL=https://your-git-repo.com/website-template.git +WEBSITE_BUILDER_TMP_DIR=./tmp +WEBSITE_BUILDER_TMP_DIR_WEBSITE_TEMPLATE=./tmp/website-template +WEBSITE_BUILDER_TMP_CUSTOMER_WEBSITES_DIR=./customer-websites \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7c294bb..0b6c5e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "express-session": "^1.17.3", + "fs-extra": "^11.2.0", "mariadb": "^3.2.3", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -34,6 +35,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/express-session": "^1.17.10", + "@types/fs-extra": "^11.0.4", "@types/passport-google-oauth20": "^2.0.14", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", @@ -203,6 +205,16 @@ "@types/express": "*" } }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, "node_modules/@types/geojson": { "version": "7946.0.13", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.13.tgz", @@ -219,6 +231,15 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, + "node_modules/@types/jsonfile": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", + "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -1166,6 +1187,19 @@ "node": ">= 0.6" } }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1272,6 +1306,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1469,6 +1508,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -2617,6 +2667,14 @@ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "engines": { + "node": ">= 10.0.0" + } + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", diff --git a/package.json b/package.json index f490424..663706a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "npx tsc", "start": "node build/server.js", - "dev": "concurrently \"npx tsc --watch\" \"nodemon -q build/server.js | pino-pretty\"" + "dev": "concurrently \"npx tsc --watch\" \"nodemon --ignore ./tmp/ --ignore ./customer-websites/ -q build/server.js | pino-pretty\"" }, "author": "", "license": "ISC", @@ -20,6 +20,7 @@ "dotenv": "^16.3.1", "express": "^4.18.2", "express-session": "^1.17.3", + "fs-extra": "^11.2.0", "mariadb": "^3.2.3", "passport": "^0.7.0", "passport-google-oauth20": "^2.0.0", @@ -36,6 +37,7 @@ "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/express-session": "^1.17.10", + "@types/fs-extra": "^11.0.4", "@types/passport-google-oauth20": "^2.0.14", "@types/swagger-jsdoc": "^6.0.4", "@types/swagger-ui-express": "^4.1.6", diff --git a/server.ts b/server.ts index 5ff08b8..6be9014 100644 --- a/server.ts +++ b/server.ts @@ -12,6 +12,7 @@ import storeRoutes from "./src/routes/storeRoutes"; import storeServicesRoutes from "./src/routes/storeServicesRoutes"; import userRoutes from "./src/routes/userRoutes"; import usersRoutes from "./src/routes/usersRoutes"; +import websiteRoutes from "./src/routes/websiteRoutes"; dotenv.config(); @@ -112,6 +113,7 @@ app.use("/api/v1/store", storeRoutes); app.use("/api/v1/store/services", storeServicesRoutes); app.use("/api/v1/user", userRoutes); app.use("/api/v1/users", usersRoutes); +app.use("/api/v1/website", websiteRoutes); const specs = swaggerJsDoc(options); app.use("/api-docs", swaggerUI.serve, swaggerUI.setup(specs)); @@ -128,8 +130,6 @@ app.use((err: any, req: any, res: any, next: any) => { syncModels(); -app.listen(port, host, () => { - //console.log(`⚡️[server]: Server is running at http://${host}:${port}`); - - logger.info(`⚡️[server]: Server is running at http://${host}:${port}`); -}); +app.listen(port, host, () => + logger.info(`⚡️[server]: Server is running at http://${host}:${port}`) +); diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 9c1531b..fe791b9 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -257,7 +257,8 @@ export async function GetUser(req: Request, res: Response) { "settings", "employees", "services", - "calendar" + "calendar", + "website" ); } diff --git a/src/controllers/websiteController.ts b/src/controllers/websiteController.ts new file mode 100644 index 0000000..e42b94f --- /dev/null +++ b/src/controllers/websiteController.ts @@ -0,0 +1,158 @@ +import { Request, Response } from "express"; +import logger from "../logger/logger"; +import util from "util"; +import { exec } from "child_process"; +import fs from "fs-extra"; +import Website from "../models/website"; +import Store from "../models/store"; + +const execPromise = util.promisify(exec); + +// this is a webhook that will be called by github when a new commit is pushed to the website template repository +export async function UpdateTemplateWebhook(req: Request, res: Response) { + try { + let tmpDirWebsiteTemplate = process.env + .WEBSITE_BUILDER_TMP_DIR_WEBSITE_TEMPLATE as string; + + logger.info("UpdateTemplateWebhook", req.body); + + // if website folder exists, pull, else clone + + if (await fs.pathExists(tmpDirWebsiteTemplate)) { + let { stdout } = await execPromise(`git pull`, { + cwd: tmpDirWebsiteTemplate, + }); + + console.log(stdout); + } else { + let { stdout } = await execPromise( + `git clone ${process.env.WEBSITE_BUILDER_TEMPLATE_REPOSITORY_URL} ${tmpDirWebsiteTemplate}` + ); + + console.log(stdout); + } + + // execute npm install + + let { stdout: installOutput } = await execPromise(`npm install`, { + cwd: tmpDirWebsiteTemplate, + }); + + console.log(installOutput); + + res.status(200).send({ message: "ok" }); + } catch (error) { + console.log("error", error); + res.status(500).send({ error: "invalid request" }); + } +} + +export async function CreateWebsite(req: Request, res: Response) { + try { + let { storeId } = req.body; + + // validate request + + if (!storeId) { + res.status(400).send({ error: "invalid request" }); + return; + } + + // check if website already exists + + const websiteExists = await Website.findOne({ + where: { + store_id: storeId, + }, + }); + + if (websiteExists) { + res.status(409).send({ error: "website already exists" }); + return; + } + + // get store name + + const store = await Store.findOne({ + where: { + store_id: storeId, + }, + attributes: ["name"], + }); + + if (!store) { + res.status(404).send({ error: "store not found" }); + return; + } + + const storeName = store.name.toLowerCase().replace(/ /g, "-"); + + // create database record + + await Website.create({ + website_id: storeName, + store_id: storeId, + }); + + const tmpDirWebsiteTemplate = process.env + .WEBSITE_BUILDER_TMP_DIR_WEBSITE_TEMPLATE as string; + const tmpDirCustomerWebsite = `${process.env.WEBSITE_BUILDER_TMP_DIR}/${storeName}`; + const customerWebsiteDir = `${process.env.WEBSITE_BUILDER_TMP_CUSTOMER_WEBSITES_DIR}/${storeName}`; + + // check if website template folder exists + + if (!(await fs.pathExists(tmpDirWebsiteTemplate))) { + logger.error("Website template folder does not exist. Clone it first."); + res.status(500).send({ error: "invalid request" }); + return; + } + + // copy website-template folder to customer website folder + await fs.copy(tmpDirWebsiteTemplate, tmpDirCustomerWebsite); + + // TODO: add config file + + // run npm build + + let { stdout: buildOutput } = await execPromise(`npm run build`, { + cwd: tmpDirCustomerWebsite, + }); + + console.log(buildOutput); + + // copy the build folder to customer websites folder + + await fs.copy(`${tmpDirCustomerWebsite}/build`, customerWebsiteDir); + + // remove tmp folder + + await fs.remove(tmpDirCustomerWebsite); + + res.status(200).send({ message: "oks" }); + } catch (error) { + console.log("error", error); + res.status(500).send({ error: "invalid request" }); + } +} + +export async function GetWebsite(req: Request, res: Response) { + try { + const { storeId } = req.params; + + const website = await Website.findOne({ + where: { + store_id: storeId, + }, + }); + + if (!website) { + res.status(404).send({ error: "website not found" }); + return; + } + + res.status(200).json({ message: "ok" }); + } catch (error) { + console.log("error", error); + res.status(500).send({ error: "invalid request" }); + } +} diff --git a/src/models/index.ts b/src/models/index.ts index 222c122..836a5ee 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -3,8 +3,8 @@ import Store from "./store"; import StoreService from "./storeService"; import StoreServiceActivity from "./storeServiceActivity"; import StoreServiceActivityUsers from "./storeServiceActivityUsers"; - import User from "./user"; +import Website from "./website"; function syncModels() { User.sync(); @@ -13,6 +13,7 @@ function syncModels() { StoreService.sync(); StoreServiceActivity.sync(); StoreServiceActivityUsers.sync(); + Website.sync(); // UserGoogleTokens.sync(); not needed as it is created by the calendar backend } diff --git a/src/models/website.ts b/src/models/website.ts new file mode 100644 index 0000000..51a6d5a --- /dev/null +++ b/src/models/website.ts @@ -0,0 +1,33 @@ +import { DataTypes, Model } from "sequelize"; +import sequelize from "../database/database"; + +interface WebsiteAttributes { + website_id: string; + store_id: string; +} + +class Website extends Model implements WebsiteAttributes { + declare website_id: string; + declare store_id: string; +} + +Website.init( + { + website_id: { + type: DataTypes.STRING, + primaryKey: true, + }, + store_id: { + type: DataTypes.STRING, + allowNull: false, + }, + }, + { + sequelize, + tableName: "websites", + createdAt: "created_at", + updatedAt: "updated_at", + } +); + +export default Website; diff --git a/src/routes/websiteRoutes.ts b/src/routes/websiteRoutes.ts new file mode 100644 index 0000000..4bf432f --- /dev/null +++ b/src/routes/websiteRoutes.ts @@ -0,0 +1,13 @@ +import express from "express"; +const router = express.Router(); + +import * as websiteController from "../controllers/websiteController"; + +router.post( + "/update-template-webhook", + websiteController.UpdateTemplateWebhook +); +router.post("/", websiteController.CreateWebsite); +router.get("/:storeId", websiteController.GetWebsite); + +export default router; diff --git a/tmp/website-template b/tmp/website-template new file mode 160000 index 0000000..3ba3fa3 --- /dev/null +++ b/tmp/website-template @@ -0,0 +1 @@ +Subproject commit 3ba3fa31410f331bf1d56b2a65eb847d918a4810