feat(backend/org): Check for existing slugs
This commit is contained in:
parent
0e3ba9907d
commit
25ebe0b0db
7 changed files with 146 additions and 11 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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}`)
|
||||
|
|
|
@ -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={
|
||||
|
|
|
@ -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))}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue