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 }) => (