1
0
Fork 0

feat(backend/org): Check for existing slugs

This commit is contained in:
Joost De Cock 2023-08-18 13:49:51 +02:00
parent 0e3ba9907d
commit 25ebe0b0db
7 changed files with 146 additions and 11 deletions

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
)
}
}

View file

@ -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}`)

View file

@ -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 = ({
<Item
title={
<div className="flex flex-row gap-2 items-center">
{slug.length > 3 ? (
{slugAvailable && slug.length > 3 ? (
<OkIcon stroke={4} className="w-5 h-5 text-success" />
) : (
<KoIcon stroke={3} className="w-5 h-5 text-error" />
@ -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.
</Tip>
<SlugInput {...{ title, slug, setSlug }} />
<SlugInput {...{ title, slug, setSlug: verifySlug, slugAvailable }} />
</Item>
<Item
title={

View file

@ -13,7 +13,9 @@ export const ns = ['account']
export const CaptionInput = ({ caption, setCaption }) => (
<input
className="input input-text input-bordered input-lg w-full"
className={`input input-text input-bordered input-lg w-full ${
caption.length < 4 ? 'input-error' : 'input-success'
}`}
value={caption}
placeholder="Type your caption here"
onChange={(evt) => setCaption(evt.target.value)}
@ -22,7 +24,9 @@ export const CaptionInput = ({ caption, setCaption }) => (
export const IntroInput = ({ intro, setIntro }) => (
<input
className="input input-text input-bordered input-lg w-full"
className={`input input-text input-bordered input-lg w-full ${
intro.length < 4 ? 'input-error' : 'input-success'
}`}
value={intro}
placeholder="Type your intro here"
onChange={(evt) => setIntro(evt.target.value)}
@ -31,7 +35,9 @@ export const IntroInput = ({ intro, setIntro }) => (
export const BodyInput = ({ body, setBody }) => (
<textarea
className="input input-text input-bordered input-lg w-full h-96"
className={`input input-text input-bordered input-lg w-full h-96 ${
body.length < 4 ? 'input-error' : 'input-success'
}`}
placeholder="Type your post body here"
onChange={(evt) => setBody(evt.target.value)}
rows={16}
@ -41,21 +47,26 @@ export const BodyInput = ({ body, setBody }) => (
export const TitleInput = ({ title, setTitle }) => (
<input
className="input input-text input-bordered input-lg w-full"
className={`input input-text input-bordered input-lg w-full ${
title.length < 11 ? 'input-error' : 'input-success'
}`}
value={title}
placeholder="Type your title here"
onChange={(evt) => setTitle(evt.target.value)}
/>
)
export const SlugInput = ({ slug, setSlug, title }) => {
export const SlugInput = ({ slug, setSlug, title, slugAvailable }) => {
useEffect(() => {
if (title !== slug) setSlug(slugify(title))
}, [title])
console.log(slugAvailable)
return (
<input
className="input input-text input-bordered input-lg w-full mb-2"
className={`input input-text input-bordered input-lg w-full mb-2 ${
!slugAvailable || slug.length < 4 ? 'input-error' : 'input-success'
}`}
value={slug}
placeholder="Type your title here"
onChange={(evt) => setSlug(slugifyNoTrim(evt.target.value))}

View file

@ -298,12 +298,25 @@ Backend.prototype.createIssue = async function (data) {
return responseHandler(await api.post(`/issues`, data), 201)
}
/*
* Check whether a slug is available
*/
Backend.prototype.isSlugAvailable = async function ({ slug, type }) {
const response = await api.get(`/slugs/${type}/${slug}/jwt`, this.auth)
// 404 means username is available, which is success in this case
return response.status === 200
? { success: false, available: false, response }
: { success: true, data: false, available: true, response }
}
/*
* Create showcase Pull Request
*/
Backend.prototype.createShowcasePr = async function (data) {
return responseHandler(await api.post(`/flows/pr/showcase/jwt`, data, this.auth), 201)
}
/*
* Send translation invite
*/