diff --git a/sites/backend/src/controllers/flows.mjs b/sites/backend/src/controllers/flows.mjs
index 9cadf833548..7ac31575c86 100644
--- a/sites/backend/src/controllers/flows.mjs
+++ b/sites/backend/src/controllers/flows.mjs
@@ -69,3 +69,24 @@ FlowsController.prototype.createIssue = async (req, res, tools) => {
return Flow.sendResponse(res)
}
+
+/*
+ * Is a blog or showcase slug available?
+ *
+ * This is the endpoint that verifies whether a blog or showcase slug is available
+ * See: https://freesewing.dev/reference/backend/api/apikey
+ */
+FlowsController.prototype.isSlugAvailable = async (req, res, tools, type) => {
+ const Flow = new FlowModel(tools)
+ const available = await Flow.isSlugAvailable(req, type)
+
+ if (!available)
+ Flow.setResponse(200, false, {
+ result: 'success',
+ slug: req.params.slug,
+ available: false,
+ })
+ else Flow.setResponse(404)
+
+ return Flow.sendResponse(res)
+}
diff --git a/sites/backend/src/models/flow.mjs b/sites/backend/src/models/flow.mjs
index b4fd06771ee..f4296500472 100644
--- a/sites/backend/src/models/flow.mjs
+++ b/sites/backend/src/models/flow.mjs
@@ -1,7 +1,13 @@
import { i18nUrl } from '../utils/index.mjs'
import { decorateModel } from '../utils/model-decorator.mjs'
import { ensureImage, replaceImage, removeImage } from '../utils/cloudflare-images.mjs'
-import { createIssue, createFile, createBranch, createPullRequest } from '../utils/github.mjs'
+import {
+ createIssue,
+ createFile,
+ createBranch,
+ createPullRequest,
+ getFileList,
+} from '../utils/github.mjs'
/*
* This model handles all flows (typically that involves sending out emails)
@@ -315,3 +321,28 @@ FlowModel.prototype.createShowcasePr = async function ({ body, user }) {
*/
return pr ? this.setResponse201({ branch, file, pr }) : this.setResponse(400)
}
+
+/*
+ * Checks to see whether a slug is available
+ *
+ * @param {params} object - The request (URL) parameters
+ * @param {user} object - The user as loaded by auth middleware
+ * @param {type} object - The type to check (either 'blog' or 'showcase')
+ * @returns {FlowModel} object - The FlowModel
+ */
+FlowModel.prototype.isSlugAvailable = async function ({ params }, type) {
+ /*
+ * Is slug set?
+ */
+ if (!params.slug) return this.setResponse(400, `slugMissing`)
+
+ /*
+ * Load the list of folders from github
+ */
+ const folders = (await getFileList(`markdown/org/${type}`)).map((folder) => folder.name)
+
+ /*
+ * Return whether or not params.slug is already included in the list of slugs
+ */
+ return !folders.includes(params.slug)
+}
diff --git a/sites/backend/src/routes/flows.mjs b/sites/backend/src/routes/flows.mjs
index 2fc319b490e..9ca39eeda69 100644
--- a/sites/backend/src/routes/flows.mjs
+++ b/sites/backend/src/routes/flows.mjs
@@ -49,4 +49,14 @@ export function flowsRoutes(tools) {
// Create Issue - No auth needed
app.post('/issues', (req, res) => Flow.createIssue(req, res, tools))
+
+ // See if a showcase or blog slug is available
+ for (const type of ['showcase', 'blog']) {
+ app.get(`/slugs/${type}/:slug/jwt`, passport.authenticate(...jwt), (req, res) =>
+ Flow.isSlugAvailable(req, res, tools, type)
+ )
+ app.get(`/slugs/${type}/:slug/key`, passport.authenticate(...bsc), (req, res) =>
+ Flow.isSlugAvailable(req, res, tools, type)
+ )
+ }
}
diff --git a/sites/backend/src/utils/github.mjs b/sites/backend/src/utils/github.mjs
index 0a24a1f12ed..66029c353ed 100644
--- a/sites/backend/src/utils/github.mjs
+++ b/sites/backend/src/utils/github.mjs
@@ -3,6 +3,40 @@ import { githubToken as token } from '../config.mjs'
const api = 'https://api.github.com/repos/freesewing/freesewing'
+/*
+ * Sometimes we'd like to cache responses.
+ * This is a poor man's cache
+ */
+const cache = {}
+const fresh = 1800000 // 30 minutes
+
+/*
+ * Helper method to run a requests against the GitHub API
+ * with built-in cachine
+ */
+const cachedApiRequest = async (method, url, body = false, success = 200) => {
+ const id = JSON.stringify({ method, url, body, success })
+ const now = Date.now()
+
+ /*
+ * Is this reponse cached?
+ */
+ if (cache[id]) {
+ /*
+ * It is. But Is it fresh?
+ */
+ if (cache[id].timestamp && now - cache[id].timestamp < fresh) return cache[id].data
+ /*
+ * It is in the cache, but stale. Remove cache entry
+ */ else delete cache[id]
+ } else {
+ const data = apiRequest(method, url, body, success)
+ cache[id] = { timestamp: now, data }
+
+ return data
+ }
+}
+
/*
* Helper method to run requests against the GitHub API
*/
@@ -79,3 +113,8 @@ export const createPullRequest = async ({ title, body, from, to = 'develop' }) =
},
201
)
+
+/*
+ * Retrieves a list of files for a folder
+ */
+export const getFileList = async (path) => await cachedApiRequest('GET', `${api}/contents/${path}`)
diff --git a/sites/org/components/github/create-showcase.mjs b/sites/org/components/github/create-showcase.mjs
index 311730378de..133ad75b222 100644
--- a/sites/org/components/github/create-showcase.mjs
+++ b/sites/org/components/github/create-showcase.mjs
@@ -59,6 +59,7 @@ export const CreateShowcasePost = () => {
const [designs, setDesigns] = useState([])
const [title, setTitle] = useState('')
const [slug, setSlug] = useState(false)
+ const [slugAvailable, setSlugAvailable] = useState(true)
const [img, setImg] = useState(false)
const [caption, setCaption] = useState('')
const [intro, setIntro] = useState('')
@@ -96,6 +97,13 @@ export const CreateShowcasePost = () => {
setExtraImages(newImages)
}
+ const verifySlug = async (newSlug) => {
+ setSlug(newSlug)
+ const result = await backend.isSlugAvailable({ slug: newSlug, type: 'showcase' })
+ console.log(result)
+ setSlugAvailable(result.available === true ? true : false)
+ }
+
const setExtraImg = (key, img) => {
const newImages = { ...extraImages }
newImages[key] = img
@@ -108,7 +116,6 @@ export const CreateShowcasePost = () => {
title,
setTitle,
slug,
- setSlug,
img,
setImg,
caption,
@@ -123,6 +130,8 @@ export const CreateShowcasePost = () => {
setExtraImg,
account,
t,
+ verifySlug,
+ slugAvailable,
}
return (
@@ -272,7 +281,8 @@ const ShowcaseEditor = ({
title,
setTitle,
slug,
- setSlug,
+ verifySlug,
+ slugAvailable,
img,
setImg,
caption,
@@ -332,7 +342,7 @@ const ShowcaseEditor = ({
-
- {slug.length > 3 ? (
+ {slugAvailable && slug.length > 3 ? (
) : (
@@ -350,7 +360,7 @@ const ShowcaseEditor = ({
The slug is the part of the URL that uniquely identifies the post. We can generate one based
on the title, but you can also customize it.
-
+
- (
setCaption(evt.target.value)}
@@ -22,7 +24,9 @@ export const CaptionInput = ({ caption, setCaption }) => (
export const IntroInput = ({ intro, setIntro }) => (
setIntro(evt.target.value)}
@@ -31,7 +35,9 @@ export const IntroInput = ({ intro, setIntro }) => (
export const BodyInput = ({ body, setBody }) => (